|
|
@@ -0,0 +1,980 @@
|
|
|
+import { FastifyInstance } from 'fastify'
|
|
|
+import { Repository } from 'typeorm'
|
|
|
+import { Api, TelegramClient } from 'telegram'
|
|
|
+import { Task, TaskStatus } from '../entities/task.entity'
|
|
|
+import { TaskItem, TaskItemStatus } from '../entities/task-item.entity'
|
|
|
+import { Sender } from '../entities/sender.entity'
|
|
|
+import { SenderService } from './sender.service'
|
|
|
+import { TgClientService } from './tgClient.service'
|
|
|
+import { ChatGroupService } from './chat-group.service'
|
|
|
+import { buildStringSession, buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
|
|
|
+
|
|
|
+export class TestService {
|
|
|
+ private readonly app: FastifyInstance
|
|
|
+ private readonly taskRepository: Repository<Task>
|
|
|
+ private readonly taskItemRepository: Repository<TaskItem>
|
|
|
+ private readonly senderRepository: Repository<Sender>
|
|
|
+ private readonly senderService: SenderService
|
|
|
+ private readonly tgClientService: TgClientService
|
|
|
+ private readonly chatGroupService: ChatGroupService
|
|
|
+
|
|
|
+ constructor(app: FastifyInstance) {
|
|
|
+ this.app = app
|
|
|
+ this.taskRepository = app.dataSource.getRepository(Task)
|
|
|
+ this.taskItemRepository = app.dataSource.getRepository(TaskItem)
|
|
|
+ this.senderRepository = app.dataSource.getRepository(Sender)
|
|
|
+ this.senderService = new SenderService(app)
|
|
|
+ this.tgClientService = TgClientService.getInstance()
|
|
|
+ this.chatGroupService = new ChatGroupService(app)
|
|
|
+ }
|
|
|
+
|
|
|
+ async findTaskById(id: number): Promise<Task | null> {
|
|
|
+ return this.taskRepository.findOne({ where: { id, delFlag: false } })
|
|
|
+ }
|
|
|
+
|
|
|
+ async testSendMessage(
|
|
|
+ senderId: string,
|
|
|
+ taskId: number,
|
|
|
+ delay?: number,
|
|
|
+ count?: number,
|
|
|
+ session?: string,
|
|
|
+ dcId?: number,
|
|
|
+ authKey?: string,
|
|
|
+ sendToVerifyAccounts?: boolean,
|
|
|
+ verifyAccounts?: string[]
|
|
|
+ ): Promise<{
|
|
|
+ success: boolean
|
|
|
+ message: string
|
|
|
+ data?: {
|
|
|
+ sender: { id: string }
|
|
|
+ task: { id: number; name: string; message: string }
|
|
|
+ totalSent: number
|
|
|
+ successCount: number
|
|
|
+ failedCount: number
|
|
|
+ }
|
|
|
+ error?: string
|
|
|
+ }> {
|
|
|
+ let client: TelegramClient | null = null
|
|
|
+
|
|
|
+ try {
|
|
|
+ if ((dcId !== undefined && authKey === undefined) || (dcId === undefined && authKey !== undefined)) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: 'dcId 和 authKey 必须同时传参'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let sender: Sender | null = null
|
|
|
+ let sessionString: string | null = null
|
|
|
+
|
|
|
+ if (session) {
|
|
|
+ try {
|
|
|
+ sessionString = buildStringSession(session)
|
|
|
+ const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ if (existingSender) {
|
|
|
+ await this.senderRepository.update(senderId, { sessionStr: sessionString })
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ } else {
|
|
|
+ sender = await this.senderService.create(senderId, undefined, undefined, sessionString)
|
|
|
+ }
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '创建或更新 sender 失败'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `解析 session 失败: ${errorMessage}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (dcId !== undefined && authKey !== undefined) {
|
|
|
+ try {
|
|
|
+ sessionString = buildStringSessionByDcIdAndAuthKey(dcId, authKey)
|
|
|
+ const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ if (existingSender) {
|
|
|
+ await this.senderRepository.update(senderId, { dcId, authKey, sessionStr: sessionString })
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ } else {
|
|
|
+ sender = await this.senderService.create(senderId, dcId, authKey, sessionString)
|
|
|
+ }
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '创建或更新 sender 失败'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `解析 session 失败: ${errorMessage}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId, delFlag: false } })
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '发送账号不存在或已被删除'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const task = await this.findTaskById(taskId)
|
|
|
+ if (!task) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '任务不存在'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const queryOptions: any = {
|
|
|
+ where: { taskId, status: TaskItemStatus.PENDING },
|
|
|
+ order: { id: 'ASC' }
|
|
|
+ }
|
|
|
+
|
|
|
+ queryOptions.take = count && count > 0 ? count : 2
|
|
|
+
|
|
|
+ const allTaskItems = await this.taskItemRepository.find(queryOptions)
|
|
|
+
|
|
|
+ if (sendToVerifyAccounts && verifyAccounts && verifyAccounts.length > 0) {
|
|
|
+ const verifyTaskItems = verifyAccounts.map(
|
|
|
+ verifyAccount =>
|
|
|
+ ({
|
|
|
+ target: verifyAccount.trim(),
|
|
|
+ isVerifyAccount: true
|
|
|
+ } as any)
|
|
|
+ )
|
|
|
+
|
|
|
+ if (verifyTaskItems.length >= 1) {
|
|
|
+ const firstVerifyAccount = verifyTaskItems[0]
|
|
|
+ const insertIndex1 = Math.min(1, allTaskItems.length)
|
|
|
+ allTaskItems.splice(insertIndex1, 0, firstVerifyAccount)
|
|
|
+ this.app.log.info(`已将验证账户插入到第二个位置: ${firstVerifyAccount.target}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (verifyTaskItems.length >= 2) {
|
|
|
+ const secondVerifyAccount = verifyTaskItems[1]
|
|
|
+ const insertIndex2 = Math.max(0, allTaskItems.length - 2)
|
|
|
+ allTaskItems.splice(insertIndex2, 0, secondVerifyAccount)
|
|
|
+ this.app.log.info(`已将验证账户插入到倒数第二个位置: ${secondVerifyAccount.target}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (verifyTaskItems.length > 2) {
|
|
|
+ const remainingVerifyAccounts = verifyTaskItems.slice(2)
|
|
|
+ allTaskItems.push(...remainingVerifyAccounts)
|
|
|
+ this.app.log.info(`已将 ${remainingVerifyAccounts.length} 个额外验证账户追加到末尾`)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(`已将 ${verifyTaskItems.length} 个验证账户插入到任务列表中`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!allTaskItems || allTaskItems.length === 0) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '该任务没有目标账户'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sessionString) {
|
|
|
+ sessionString = sender!.sessionStr
|
|
|
+ if (!sessionString) {
|
|
|
+ if (!sender!.dcId || !sender!.authKey) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: 'Sender 缺少 dcId 或 authKey 信息'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sessionString = buildStringSessionByDcIdAndAuthKey(sender!.dcId, sender!.authKey)
|
|
|
+ await this.senderRepository.update(sender!.id, { sessionStr: sessionString })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info('正在连接 TelegramClient...')
|
|
|
+ client = await this.tgClientService.connect(sessionString)
|
|
|
+ this.app.log.info('TelegramClient 连接完成')
|
|
|
+
|
|
|
+ const waitTime = Math.floor(Math.random() * 21) + 20
|
|
|
+ this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
|
|
|
+ this.app.log.info(`等待完成,开始发送消息`)
|
|
|
+
|
|
|
+ let totalSuccessCount = 0
|
|
|
+ let totalFailedCount = 0
|
|
|
+ let totalSentCount = 0
|
|
|
+
|
|
|
+ const useRandomDelay = delay === undefined || delay <= 0
|
|
|
+ const fixedDelay = delay && delay > 0 ? delay : 0
|
|
|
+
|
|
|
+ this.app.log.info(
|
|
|
+ `开始测试发送: sender=${senderId}, task=${taskId}, 目标数=${allTaskItems.length}, 延迟=${
|
|
|
+ useRandomDelay ? '随机(3-10秒)' : `${fixedDelay}秒`
|
|
|
+ }`
|
|
|
+ )
|
|
|
+
|
|
|
+ for (const taskItem of allTaskItems) {
|
|
|
+ try {
|
|
|
+ const parsedTarget = this.parseTarget(taskItem.target)
|
|
|
+ if (!parsedTarget) {
|
|
|
+ throw new Error('target 格式错误,请检查是否正确')
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetPeer = await this.tgClientService.getTargetPeer(client, parsedTarget)
|
|
|
+ if (!targetPeer) {
|
|
|
+ throw new Error('target 无效,无法获取目标信息')
|
|
|
+ }
|
|
|
+
|
|
|
+ const canSendMessage = await this.checkCanSendMessage(client, targetPeer)
|
|
|
+ if (!canSendMessage) {
|
|
|
+ throw new Error('目标用户不允许接收消息或已被限制')
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.tgClientService.sendMessageToPeer(client, targetPeer, task.message)
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.tgClientService.clearConversation(client, targetPeer)
|
|
|
+ } catch (clearError) {
|
|
|
+ this.app.log.warn(
|
|
|
+ `清除会话失败 [${taskItem.target}]: ${clearError instanceof Error ? clearError.message : '未知错误'}`
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.tgClientService.deleteTempContact(client, targetPeer.id)
|
|
|
+ } catch (deleteError) {
|
|
|
+ this.app.log.warn(
|
|
|
+ `删除临时联系人失败 [${taskItem.target}]: ${
|
|
|
+ deleteError instanceof Error ? deleteError.message : '未知错误'
|
|
|
+ }`
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const isVerifyAccount = (taskItem as any).isVerifyAccount === true
|
|
|
+
|
|
|
+ if (isVerifyAccount) {
|
|
|
+ this.app.log.info(`[验证账户] 发送成功: ${taskItem.target}`)
|
|
|
+ } else {
|
|
|
+ if (taskItem.id) {
|
|
|
+ try {
|
|
|
+ await this.taskItemRepository.update(taskItem.id, {
|
|
|
+ status: TaskItemStatus.SUCCESS,
|
|
|
+ sentAt: new Date()
|
|
|
+ })
|
|
|
+ } catch (updateError) {
|
|
|
+ const updateErrorMessage = updateError instanceof Error ? updateError.message : '未知错误'
|
|
|
+ this.app.log.warn(`更新 taskItem 状态失败 [${taskItem.target}]: ${updateErrorMessage}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ totalSuccessCount++
|
|
|
+ } catch (error) {
|
|
|
+ const isVerifyAccount = (taskItem as any).isVerifyAccount === true
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+
|
|
|
+ if (isVerifyAccount) {
|
|
|
+ this.app.log.warn(`[验证账户] 发送失败 [${taskItem.target}]: ${errorMessage}`)
|
|
|
+ } else {
|
|
|
+ if (taskItem.id) {
|
|
|
+ try {
|
|
|
+ await this.taskItemRepository.update(taskItem.id, {
|
|
|
+ status: TaskItemStatus.FAILED,
|
|
|
+ sentAt: new Date()
|
|
|
+ })
|
|
|
+ } catch (updateError) {
|
|
|
+ const updateErrorMessage = updateError instanceof Error ? updateError.message : '未知错误'
|
|
|
+ this.app.log.warn(`更新 taskItem 失败状态失败 [${taskItem.target}]: ${updateErrorMessage}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.app.log.warn(`发送失败 [${taskItem.target}]: ${errorMessage}`)
|
|
|
+ }
|
|
|
+ totalFailedCount++
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.senderService.incrementUsageCount(senderId)
|
|
|
+ } catch (error) {
|
|
|
+ this.app.log.warn(
|
|
|
+ `更新 sender usageCount 失败 [${senderId}]: ${error instanceof Error ? error.message : '未知错误'}`
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ totalSentCount++
|
|
|
+
|
|
|
+ if (totalSentCount < allTaskItems.length) {
|
|
|
+ let actualDelay: number
|
|
|
+ if (useRandomDelay) {
|
|
|
+ actualDelay = Math.floor(Math.random() * 8) + 3
|
|
|
+ } else {
|
|
|
+ actualDelay = fixedDelay
|
|
|
+ }
|
|
|
+ this.app.log.info(`等待延迟 ${actualDelay} 秒后发送下一条消息`)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, actualDelay * 1000))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(`所有消息发送完成: 总计=${totalSentCount}, 成功=${totalSuccessCount}, 失败=${totalFailedCount}`)
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: `测试发送完成,共发送 ${totalSentCount} 条,成功 ${totalSuccessCount} 条,失败 ${totalFailedCount} 条`,
|
|
|
+ data: {
|
|
|
+ sender: { id: sender!.id },
|
|
|
+ task: { id: task.id, name: task.name, message: task.message },
|
|
|
+ totalSent: totalSentCount,
|
|
|
+ successCount: totalSuccessCount,
|
|
|
+ failedCount: totalFailedCount
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`测试发送失败: ${errorMessage}`)
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '测试发送消息时发生错误',
|
|
|
+ error: errorMessage
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (client) {
|
|
|
+ try {
|
|
|
+ this.app.log.info(`正在断开连接并销毁 client`)
|
|
|
+ await this.tgClientService.disconnect()
|
|
|
+ this.app.log.info(`连接已断开,client 已销毁`)
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`断开连接失败: ${errorMessage}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async testCreateChatGroup(
|
|
|
+ senderId: string,
|
|
|
+ groupName: string,
|
|
|
+ groupDescription: string,
|
|
|
+ groupType: string,
|
|
|
+ session?: string,
|
|
|
+ dcId?: number,
|
|
|
+ authKey?: string,
|
|
|
+ initMsg?: string
|
|
|
+ ): Promise<any> {
|
|
|
+ let client: TelegramClient | null = null
|
|
|
+
|
|
|
+ try {
|
|
|
+ const sessionResult = await this.resolveSenderSession(senderId, session, dcId, authKey)
|
|
|
+ if ('error' in sessionResult) {
|
|
|
+ return sessionResult.error
|
|
|
+ }
|
|
|
+ const { sessionString } = sessionResult
|
|
|
+
|
|
|
+ this.app.log.info('正在连接 TelegramClient...')
|
|
|
+ client = await this.tgClientService.connect(sessionString)
|
|
|
+ this.app.log.info('TelegramClient 连接完成')
|
|
|
+
|
|
|
+ const waitTime = Math.floor(Math.random() * 21) + 20
|
|
|
+ this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
|
|
|
+
|
|
|
+ this.app.log.info(`等待完成,开始创建群组`)
|
|
|
+
|
|
|
+ if (groupType !== 'channel' && groupType !== 'megagroup') {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `不支持的群组类型: ${groupType},仅支持: megagroup(超级群组), channel(频道)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(`正在创建${groupType === 'channel' ? '频道' : '超级群组'}: ${groupName}`)
|
|
|
+
|
|
|
+ const result = await client.invoke(
|
|
|
+ new Api.channels.CreateChannel({
|
|
|
+ title: groupName,
|
|
|
+ about: groupDescription || '',
|
|
|
+ megagroup: groupType === 'megagroup'
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const updates = result as any
|
|
|
+ const createdChat = updates.chats?.[0]
|
|
|
+
|
|
|
+ if (!createdChat) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '创建群组失败,未返回群组信息'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const chatId = createdChat.id
|
|
|
+ const accessHash = createdChat.accessHash
|
|
|
+
|
|
|
+ this.app.log.info(`群组创建成功,ID: ${chatId}, accessHash: ${accessHash}`)
|
|
|
+
|
|
|
+ const inputChannel = new Api.InputChannel({
|
|
|
+ channelId: chatId,
|
|
|
+ accessHash: accessHash
|
|
|
+ })
|
|
|
+
|
|
|
+ const shouldSendInit = typeof initMsg === 'string' && initMsg.trim().length > 0
|
|
|
+ if (shouldSendInit) {
|
|
|
+ try {
|
|
|
+ await client.sendMessage(inputChannel, {
|
|
|
+ message: initMsg.trim()
|
|
|
+ })
|
|
|
+ this.app.log.info('已向群组发送自定义欢迎消息')
|
|
|
+ } catch (e) {
|
|
|
+ this.app.log.warn('发送欢迎消息失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.app.log.info('未提供 initMsg,跳过欢迎消息发送')
|
|
|
+ }
|
|
|
+
|
|
|
+ let publicLink: string | null = null
|
|
|
+ let inviteLinkPermanent: string | null = null
|
|
|
+ try {
|
|
|
+ const fullChannel = await client.invoke(
|
|
|
+ new Api.channels.GetFullChannel({
|
|
|
+ channel: inputChannel
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const channel = fullChannel.chats?.[0] as any
|
|
|
+ let channelUsername: string | undefined = channel?.username
|
|
|
+
|
|
|
+ if (!channelUsername) {
|
|
|
+ const candidate = `group_${Date.now()}`
|
|
|
+ let desiredHandle = candidate.toLowerCase()
|
|
|
+ if (desiredHandle.length > 32) desiredHandle = desiredHandle.slice(0, 32)
|
|
|
+ if (desiredHandle.length < 5) desiredHandle = desiredHandle.padEnd(5, '0')
|
|
|
+
|
|
|
+ try {
|
|
|
+ await client.invoke(
|
|
|
+ new Api.channels.UpdateUsername({
|
|
|
+ channel: inputChannel,
|
|
|
+ username: desiredHandle
|
|
|
+ })
|
|
|
+ )
|
|
|
+ channelUsername = desiredHandle
|
|
|
+ this.app.log.info(`已为群设置用户名: ${channelUsername}`)
|
|
|
+ } catch (setError) {
|
|
|
+ const msg = setError instanceof Error ? setError.message : '未知错误'
|
|
|
+ this.app.log.warn(`为群设置用户名失败: ${msg}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (channelUsername) {
|
|
|
+ publicLink = `https://t.me/${channelUsername}`
|
|
|
+ this.app.log.info(`群组公开链接(永久): ${publicLink}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const inviteLink = await client.invoke(
|
|
|
+ new Api.messages.ExportChatInvite({
|
|
|
+ peer: inputChannel
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const invite = inviteLink as any
|
|
|
+ if (invite?.link) {
|
|
|
+ inviteLinkPermanent = invite.link
|
|
|
+ this.app.log.info(`群组邀请链接(永久): ${inviteLinkPermanent}`)
|
|
|
+ } else {
|
|
|
+ this.app.log.warn('未能获取群组邀请链接')
|
|
|
+ }
|
|
|
+ } catch (inviteError) {
|
|
|
+ const msg = inviteError instanceof Error ? inviteError.message : '未知错误'
|
|
|
+ this.app.log.warn(`获取群组邀请链接失败: ${msg}`)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.warn(`获取群组链接失败: ${errorMessage}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.chatGroupService.upsertGroup({
|
|
|
+ chatId: chatId.toString(),
|
|
|
+ accessHash: accessHash?.toString() || '',
|
|
|
+ name: createdChat.title || groupName,
|
|
|
+ groupType,
|
|
|
+ publicLink: publicLink || undefined,
|
|
|
+ inviteLink: inviteLinkPermanent || undefined,
|
|
|
+ senderId
|
|
|
+ })
|
|
|
+ this.app.log.info('群组信息已保存到数据库')
|
|
|
+ } catch (saveError) {
|
|
|
+ const msg = saveError instanceof Error ? saveError.message : '未知错误'
|
|
|
+ this.app.log.warn(`保存群组信息失败: ${msg}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: '群组创建成功',
|
|
|
+ data: {
|
|
|
+ chatId: chatId.toString(),
|
|
|
+ chatTitle: groupName,
|
|
|
+ chatType: groupType,
|
|
|
+ groupLinkPublic: publicLink || null,
|
|
|
+ groupInviteLink: inviteLinkPermanent || null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`创建聊天群失败: ${errorMessage}`)
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `创建聊天群失败: ${errorMessage}`
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (client) {
|
|
|
+ try {
|
|
|
+ await this.tgClientService.disconnect()
|
|
|
+ this.app.log.info('TelegramClient 已断开连接')
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`断开连接失败: ${errorMessage}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async testInviteMembersToChat(
|
|
|
+ senderId: string,
|
|
|
+ chatId: string,
|
|
|
+ members: string[],
|
|
|
+ accessHash?: string
|
|
|
+ ): Promise<{
|
|
|
+ success: boolean
|
|
|
+ message: string
|
|
|
+ data?: { chatId: string; inviteResults: { total: number; success: number; failed: number; errors: string[] } }
|
|
|
+ }> {
|
|
|
+ let client: TelegramClient | null = null
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!members || members.length === 0) {
|
|
|
+ return { success: false, message: '成员列表不能为空' }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!chatId || !chatId.trim()) {
|
|
|
+ return { success: false, message: 'chatId 不能为空' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const sessionResult = await this.resolveSenderSession(senderId)
|
|
|
+ if ('error' in sessionResult) {
|
|
|
+ return sessionResult.error
|
|
|
+ }
|
|
|
+ const { sessionString } = sessionResult
|
|
|
+
|
|
|
+ this.app.log.info('正在连接 TelegramClient...')
|
|
|
+ client = await this.tgClientService.connect(sessionString)
|
|
|
+ this.app.log.info('TelegramClient 连接完成')
|
|
|
+
|
|
|
+ const waitTime = Math.floor(Math.random() * 21) + 20
|
|
|
+ this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
|
|
|
+ this.app.log.info('等待完成,开始邀请成员')
|
|
|
+
|
|
|
+ let successCount = 0
|
|
|
+ let failedCount = 0
|
|
|
+ const errors: string[] = []
|
|
|
+
|
|
|
+ const chatTarget = chatId.trim()
|
|
|
+ const accessHashTrimmed = accessHash?.trim()
|
|
|
+ const channelIdWithHashMatch = chatTarget.match(/^(-?\d+)[,:#](\d+)$/)
|
|
|
+ let chatEntity: any
|
|
|
+ try {
|
|
|
+ if (channelIdWithHashMatch) {
|
|
|
+ const [, idStr, accessHashStr] = channelIdWithHashMatch
|
|
|
+ chatEntity = {
|
|
|
+ id: BigInt(idStr),
|
|
|
+ accessHash: BigInt(accessHashStr),
|
|
|
+ className: 'Channel',
|
|
|
+ title: chatTarget
|
|
|
+ }
|
|
|
+ this.app.log.info('已使用显式 channelId + accessHash 初始化频道实体')
|
|
|
+ } else if (accessHashTrimmed && /^-?\d+$/.test(chatTarget)) {
|
|
|
+ chatEntity = {
|
|
|
+ id: BigInt(chatTarget),
|
|
|
+ accessHash: BigInt(accessHashTrimmed),
|
|
|
+ className: 'Channel',
|
|
|
+ title: chatTarget
|
|
|
+ }
|
|
|
+ this.app.log.info('已使用 chatId + accessHash 初始化频道实体')
|
|
|
+ } else {
|
|
|
+ const parsedChatId = /^-?\d+$/.test(chatTarget) ? Number(chatTarget) : chatTarget
|
|
|
+ chatEntity = await client.getEntity(parsedChatId)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (/^\d+$/.test(chatTarget)) {
|
|
|
+ try {
|
|
|
+ const prefixedChannelId = BigInt(`-100${chatTarget}`)
|
|
|
+ chatEntity = await client.getEntity(prefixedChannelId.toString())
|
|
|
+ this.app.log.info(`通过 -100 前缀重试获取群组成功: ${prefixedChannelId.toString()}`)
|
|
|
+ } catch (error2) {
|
|
|
+ const errorMessage2 = error2 instanceof Error ? error2.message : '未知错误'
|
|
|
+ this.app.log.error(`获取群组信息失败(二次尝试): ${errorMessage2}`)
|
|
|
+ return { success: false, message: `获取群组信息失败: ${errorMessage2}` }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`获取群组信息失败: ${errorMessage}`)
|
|
|
+ return { success: false, message: `获取群组信息失败: ${errorMessage}` }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(
|
|
|
+ `已获取群组信息: ${chatEntity.title || chatEntity.username || chatTarget} (class: ${chatEntity.className})`
|
|
|
+ )
|
|
|
+
|
|
|
+ const isChannel = chatEntity?.className === 'Channel'
|
|
|
+ const isChat = chatEntity?.className === 'Chat'
|
|
|
+ if (!isChannel && !isChat) {
|
|
|
+ return { success: false, message: '目标并非群组或频道,无法邀请成员' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const inviteToGroup = async (inputUser: Api.InputUser) => {
|
|
|
+ if (isChannel) {
|
|
|
+ if (!chatEntity?.accessHash) {
|
|
|
+ throw new Error('缺少 accessHash,无法邀请到频道/超级群组')
|
|
|
+ }
|
|
|
+ const inputChannel = new Api.InputChannel({
|
|
|
+ channelId: chatEntity.id,
|
|
|
+ accessHash: chatEntity.accessHash
|
|
|
+ })
|
|
|
+ await client!.invoke(
|
|
|
+ new Api.channels.InviteToChannel({
|
|
|
+ channel: inputChannel,
|
|
|
+ users: [inputUser]
|
|
|
+ })
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ await client!.invoke(
|
|
|
+ new Api.messages.AddChatUser({
|
|
|
+ chatId: chatEntity.id,
|
|
|
+ userId: inputUser,
|
|
|
+ fwdLimit: 0
|
|
|
+ })
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 0; i < members.length; i++) {
|
|
|
+ const member = members[i]
|
|
|
+ let lastError: string | null = null
|
|
|
+
|
|
|
+ this.app.log.info(`正在邀请第 ${i + 1}/${members.length} 个成员: ${member}`)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const parsedTarget = this.parseTarget(member)
|
|
|
+ if (!parsedTarget) {
|
|
|
+ throw new Error(`成员格式错误: ${member}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ let targetUser: any
|
|
|
+ try {
|
|
|
+ targetUser = await this.tgClientService.getTargetPeer(client, parsedTarget)
|
|
|
+ if (!targetUser) {
|
|
|
+ throw new Error('无法获取用户信息')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ throw new Error(`获取用户信息失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const inputUser = new Api.InputUser({
|
|
|
+ userId: targetUser.id,
|
|
|
+ accessHash: targetUser.accessHash || BigInt(0)
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ await inviteToGroup(inputUser)
|
|
|
+ this.app.log.info(`成功邀请成员: ${member}`)
|
|
|
+ successCount++
|
|
|
+ } catch (inviteError) {
|
|
|
+ const inviteErrorMessage = inviteError instanceof Error ? inviteError.message : '未知错误'
|
|
|
+
|
|
|
+ if (inviteErrorMessage.includes('USER_ALREADY_PARTICIPANT')) {
|
|
|
+ this.app.log.info(`成员已在群组中: ${member}`)
|
|
|
+ successCount++
|
|
|
+ } else if (inviteErrorMessage.includes('PEER_FLOOD')) {
|
|
|
+ throw new Error('邀请频率过快,触发 PEER_FLOOD 限制,请降低邀请频率')
|
|
|
+ } else if (inviteErrorMessage.includes('USER_PRIVACY_RESTRICTED')) {
|
|
|
+ throw new Error('用户隐私设置不允许被邀请')
|
|
|
+ } else if (inviteErrorMessage.includes('USER_NOT_MUTUAL_CONTACT')) {
|
|
|
+ throw new Error('用户不是双向联系人,无法邀请')
|
|
|
+ } else if (inviteErrorMessage.includes('USER_CHANNELS_TOO_MUCH')) {
|
|
|
+ throw new Error('用户加入的群组/频道数量已达上限')
|
|
|
+ } else if (inviteErrorMessage.includes('INVITE_REQUEST_SENT')) {
|
|
|
+ this.app.log.info(`已向用户发送邀请请求: ${member}`)
|
|
|
+ successCount++
|
|
|
+ } else {
|
|
|
+ throw inviteError
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ const errorDetail = `${member}: ${errorMessage}`
|
|
|
+ this.app.log.error(`邀请成员失败: ${errorDetail}`)
|
|
|
+ errors.push(errorDetail)
|
|
|
+ failedCount++
|
|
|
+ lastError = errorMessage
|
|
|
+ } finally {
|
|
|
+ if (i < members.length - 1) {
|
|
|
+ let delayTime: number
|
|
|
+ let delayReason: string
|
|
|
+
|
|
|
+ if (lastError) {
|
|
|
+ if (
|
|
|
+ lastError.includes('FLOOD_WAIT') ||
|
|
|
+ lastError.includes('SLOWMODE_WAIT') ||
|
|
|
+ lastError.includes('PEER_FLOOD') ||
|
|
|
+ lastError.includes('Too Many Requests')
|
|
|
+ ) {
|
|
|
+ delayTime = 120
|
|
|
+ delayReason = `检测到限制(${lastError.includes('PEER_FLOOD') ? 'PEER_FLOOD' : 'FLOOD_WAIT'})`
|
|
|
+ } else {
|
|
|
+ delayTime = Math.floor(Math.random() * 11) + 30
|
|
|
+ delayReason = '普通错误后等待'
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ delayTime = Math.floor(Math.random() * 11) + 30
|
|
|
+ delayReason = '避免 Peer Flood'
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(`${delayReason},等待 ${delayTime} 秒后继续邀请下一个成员`)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, delayTime * 1000))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.chatGroupService.upsertGroup({
|
|
|
+ chatId: chatEntity.id.toString(),
|
|
|
+ accessHash: (chatEntity.accessHash || '').toString(),
|
|
|
+ name: chatEntity.title || chatEntity.username || chatTarget,
|
|
|
+ groupType: isChannel ? 'channel' : 'chat',
|
|
|
+ senderId
|
|
|
+ })
|
|
|
+ } catch (saveError) {
|
|
|
+ const msg = saveError instanceof Error ? saveError.message : '未知错误'
|
|
|
+ this.app.log.warn(`保存群组信息失败: ${msg}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: '邀请成员完成',
|
|
|
+ data: {
|
|
|
+ chatId: chatTarget,
|
|
|
+ inviteResults: {
|
|
|
+ total: members.length,
|
|
|
+ success: successCount,
|
|
|
+ failed: failedCount,
|
|
|
+ errors
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`邀请成员到群失败: ${errorMessage}`)
|
|
|
+ return { success: false, message: `邀请成员失败: ${errorMessage}` }
|
|
|
+ } finally {
|
|
|
+ if (client) {
|
|
|
+ try {
|
|
|
+ await this.tgClientService.disconnect()
|
|
|
+ this.app.log.info('TelegramClient 已断开连接')
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ this.app.log.error(`断开连接失败: ${errorMessage}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async resolveSenderSession(
|
|
|
+ senderId: string,
|
|
|
+ session?: string,
|
|
|
+ dcId?: number,
|
|
|
+ authKey?: string
|
|
|
+ ): Promise<{ sender: Sender; sessionString: string } | { error: { success: false; message: string } }> {
|
|
|
+ if ((dcId !== undefined && authKey === undefined) || (dcId === undefined && authKey !== undefined)) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: 'dcId 和 authKey 必须同时传参'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let sender: Sender | null = null
|
|
|
+ let sessionString: string | null = null
|
|
|
+
|
|
|
+ if (session) {
|
|
|
+ try {
|
|
|
+ sessionString = buildStringSession(session)
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: `解析 session 失败: ${errorMessage}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ if (existingSender) {
|
|
|
+ await this.senderRepository.update(senderId, { sessionStr: sessionString })
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ } else {
|
|
|
+ sender = await this.senderService.create(senderId, undefined, undefined, sessionString)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: '创建或更新 sender 失败'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (dcId !== undefined && authKey !== undefined) {
|
|
|
+ try {
|
|
|
+ sessionString = buildStringSessionByDcIdAndAuthKey(dcId, authKey)
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: `解析 session 失败: ${errorMessage}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ if (existingSender) {
|
|
|
+ await this.senderRepository.update(senderId, { dcId, authKey, sessionStr: sessionString })
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId } })
|
|
|
+ } else {
|
|
|
+ sender = await this.senderService.create(senderId, dcId, authKey, sessionString)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: '创建或更新 sender 失败'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ sender = await this.senderRepository.findOne({ where: { id: senderId, delFlag: false } })
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: '发送账号不存在或已被删除'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sender) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: '发送账号不存在或已被删除'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sessionString) {
|
|
|
+ sessionString = sender.sessionStr
|
|
|
+ if (!sessionString) {
|
|
|
+ if (!sender.dcId || !sender.authKey) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: 'Sender 缺少 dcId 或 authKey 信息'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sessionString = buildStringSessionByDcIdAndAuthKey(sender.dcId, sender.authKey)
|
|
|
+ await this.senderRepository.update(sender.id, { sessionStr: sessionString })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sessionString) {
|
|
|
+ return {
|
|
|
+ error: {
|
|
|
+ success: false,
|
|
|
+ message: '无法生成 session 信息'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ sender,
|
|
|
+ sessionString
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseTarget(targetId: string): string | number | null {
|
|
|
+ const trimmed = targetId.trim()
|
|
|
+
|
|
|
+ if (trimmed.startsWith('@') || trimmed.startsWith('+')) {
|
|
|
+ return trimmed
|
|
|
+ }
|
|
|
+
|
|
|
+ const phoneRegex = /^\d+$/
|
|
|
+ if (phoneRegex.test(trimmed)) {
|
|
|
+ return `+${trimmed}`
|
|
|
+ }
|
|
|
+
|
|
|
+ const integerRegex = /^-?\d+$/
|
|
|
+ if (integerRegex.test(trimmed)) {
|
|
|
+ return Number(trimmed)
|
|
|
+ }
|
|
|
+
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ private async checkCanSendMessage(client: TelegramClient, targetPeer: any): Promise<boolean> {
|
|
|
+ try {
|
|
|
+ const fullUser = await client.invoke(
|
|
|
+ new Api.users.GetFullUser({
|
|
|
+ id: targetPeer
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const fullUserData = fullUser.fullUser as any
|
|
|
+
|
|
|
+ if (fullUserData?.blocked) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetPeer.bot && targetPeer.botChatHistory === false) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetPeer.deleted) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetPeer.fake || targetPeer.scam) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
|
+
|
|
|
+ if (errorMessage.includes('AUTH_KEY_UNREGISTERED')) {
|
|
|
+ throw new Error('认证密钥未注册,请检查 session 是否有效或需要重新授权')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (errorMessage.includes('PRIVACY') || errorMessage.includes('USER_PRIVACY_RESTRICTED')) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|