|
|
@@ -1,45 +1,14 @@
|
|
|
-import { Repository } from 'typeorm'
|
|
|
-import { TelegramClient } from 'telegram'
|
|
|
import { FastifyInstance } from 'fastify'
|
|
|
-import { Api } from 'telegram'
|
|
|
import { SendMessageResult } from '../dto/tg-msg-send.dto'
|
|
|
import { TgClientService } from './tgClient.service'
|
|
|
-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 { buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
|
|
|
|
|
|
export class TgMsgSendService {
|
|
|
private app: FastifyInstance
|
|
|
private tgClientService: TgClientService
|
|
|
- private taskRepository: Repository<Task>
|
|
|
- private taskItemRepository: Repository<TaskItem>
|
|
|
- private senderRepository: Repository<Sender>
|
|
|
- private senderService: SenderService
|
|
|
- private processing = false
|
|
|
- private static schedulerStarted = false
|
|
|
- private readonly pollIntervalMs = 5000
|
|
|
- private readonly instanceId = `${process.pid}-${Math.random().toString(36).slice(2, 8)}`
|
|
|
- private processingSince: number | null = null
|
|
|
- private readonly maxProcessingMs = 2 * 60 * 1000
|
|
|
- private readonly defaultSenderSendLimit = 5
|
|
|
- private currentSenderSendLimit = this.defaultSenderSendLimit
|
|
|
- private senderUsageInBatch: Map<string, number> = new Map()
|
|
|
- private senderCursor = 0
|
|
|
- private senderCache: Sender[] = []
|
|
|
- private readonly taskBatchSize = 50
|
|
|
|
|
|
constructor(app: FastifyInstance) {
|
|
|
this.app = app
|
|
|
- this.tgClientService = TgClientService.getInstance()
|
|
|
- this.taskRepository = app.dataSource.getRepository(Task)
|
|
|
- this.taskItemRepository = app.dataSource.getRepository(TaskItem)
|
|
|
- this.senderRepository = app.dataSource.getRepository(Sender)
|
|
|
- this.senderService = new SenderService(app)
|
|
|
- this.app.addHook('onReady', async () => {
|
|
|
- this.scheduleTaskSend()
|
|
|
- })
|
|
|
+ this.tgClientService = new TgClientService()
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -55,33 +24,30 @@ export class TgMsgSendService {
|
|
|
return { success: false, error: configError }
|
|
|
}
|
|
|
|
|
|
- let client: TelegramClient | null = null
|
|
|
-
|
|
|
try {
|
|
|
- client = await this.tgClientService.connect(sessionString)
|
|
|
+ await this.tgClientService.connect(sessionString)
|
|
|
|
|
|
const parsedTarget = this.parseTarget(target)
|
|
|
if (!parsedTarget) {
|
|
|
return { success: false, error: 'target 格式错误,请检查是否正确' }
|
|
|
}
|
|
|
|
|
|
- const targetPeer = await this.tgClientService.getTargetPeer(client, parsedTarget)
|
|
|
+ const targetPeer = await this.tgClientService.getTargetPeer(parsedTarget)
|
|
|
if (!targetPeer) {
|
|
|
return { success: false, error: 'target 无效,无法获取目标信息' }
|
|
|
}
|
|
|
|
|
|
- const result = await this.tgClientService.sendMessageToPeer(client, targetPeer, message)
|
|
|
+ const result = await this.tgClientService.sendMessageToPeer(targetPeer, message)
|
|
|
this.app.log.info(`✅ 消息发送成功`, result.id)
|
|
|
|
|
|
- await this.tgClientService.clearConversation(client, targetPeer)
|
|
|
+ await this.tgClientService.clearConversation(targetPeer)
|
|
|
this.app.log.info('会话清除成功')
|
|
|
|
|
|
if (target.startsWith('+')) {
|
|
|
- await this.tgClientService.deleteTempContact(client, targetPeer.id)
|
|
|
+ await this.tgClientService.deleteTempContact(targetPeer.id)
|
|
|
this.app.log.info('临时联系人删除成功')
|
|
|
}
|
|
|
|
|
|
- await this.tgClientService.disconnect()
|
|
|
this.app.log.info('=============发送消息任务结束=============')
|
|
|
|
|
|
return {
|
|
|
@@ -95,6 +61,8 @@ export class TgMsgSendService {
|
|
|
success: false,
|
|
|
error: errorMessage
|
|
|
}
|
|
|
+ } finally {
|
|
|
+ await this.tgClientService.disconnect()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -155,497 +123,4 @@ export class TgMsgSendService {
|
|
|
}
|
|
|
return '未知错误'
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 启动任务发送轮询调度器
|
|
|
- * 使用静态变量确保只启动一个轮询实例
|
|
|
- */
|
|
|
- private scheduleTaskSend() {
|
|
|
- if (TgMsgSendService.schedulerStarted) {
|
|
|
- return
|
|
|
- }
|
|
|
- const interval = setInterval(() => void this.taskSendCycle(), this.pollIntervalMs)
|
|
|
- TgMsgSendService.schedulerStarted = true
|
|
|
-
|
|
|
- this.app.addHook('onClose', async () => {
|
|
|
- clearInterval(interval)
|
|
|
- TgMsgSendService.schedulerStarted = false
|
|
|
- })
|
|
|
-
|
|
|
- this.app.log.info(`任务发送轮询已启动,间隔=${this.pollIntervalMs}ms,实例=${this.instanceId}`)
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 任务发送轮询循环
|
|
|
- * 每轮询一次处理一个发送中的任务,包含防卡死机制
|
|
|
- */
|
|
|
- private async taskSendCycle() {
|
|
|
- if (this.processing) {
|
|
|
- const now = Date.now()
|
|
|
- if (this.processingSince && now - this.processingSince > this.maxProcessingMs) {
|
|
|
- this.app.log.warn('taskSendCycle 脱离卡死,重置 processing')
|
|
|
- this.processing = false
|
|
|
- } else {
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- this.processing = true
|
|
|
- this.processingSince = Date.now()
|
|
|
- try {
|
|
|
- await this.processTaskSend()
|
|
|
- } catch (error) {
|
|
|
- const msg = error instanceof Error ? error.message : '未知错误'
|
|
|
- const isDbConnectionError =
|
|
|
- msg.includes('ECONNRESET') ||
|
|
|
- msg.includes('EPIPE') ||
|
|
|
- msg.includes('ETIMEDOUT') ||
|
|
|
- msg.includes('ENOTFOUND') ||
|
|
|
- msg.includes('Connection lost') ||
|
|
|
- msg.includes('Connection closed')
|
|
|
-
|
|
|
- if (isDbConnectionError) {
|
|
|
- this.app.log.debug(`数据库连接异常,跳过本次轮询: ${msg}`)
|
|
|
- } else {
|
|
|
- const stack = error instanceof Error ? error.stack : undefined
|
|
|
- this.app.log.error(`处理发送任务失败: ${msg}${stack ? `; stack=${stack}` : ''}`)
|
|
|
- }
|
|
|
- } finally {
|
|
|
- this.processing = false
|
|
|
- this.processingSince = null
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理任务发送
|
|
|
- * 查找一个发送中的任务,批量处理待发送的任务项,支持并发发送
|
|
|
- */
|
|
|
- private async processTaskSend() {
|
|
|
- const task = await this.taskRepository.findOne({
|
|
|
- where: { status: TaskStatus.SENDING, delFlag: false },
|
|
|
- order: { startedAt: 'ASC' }
|
|
|
- })
|
|
|
-
|
|
|
- if (!task) {
|
|
|
- return
|
|
|
- }
|
|
|
- this.app.log.info(`开始发送任务 id=${task.id}, startedAt=${task.startedAt?.toISOString}`)
|
|
|
-
|
|
|
- const configuredSendLimit =
|
|
|
- task.sendLimit && Number(task.sendLimit) > 0 ? Number(task.sendLimit) : this.defaultSenderSendLimit
|
|
|
- const sendIntervalMs = Math.max(0, Number(task.sendInterval ?? 0) * 1000)
|
|
|
- const batchSize =
|
|
|
- task.sendBatchSize && Number(task.sendBatchSize) > 0 ? Number(task.sendBatchSize) : this.taskBatchSize
|
|
|
- const concurrentCount = task.concurrentCount && Number(task.concurrentCount) > 0 ? Number(task.concurrentCount) : 1
|
|
|
- const batchTotal = batchSize
|
|
|
-
|
|
|
- this.currentSenderSendLimit = configuredSendLimit
|
|
|
- this.senderUsageInBatch.clear()
|
|
|
- this.senderCursor = 0
|
|
|
- await this.refreshSenderCache()
|
|
|
-
|
|
|
- const pendingItems = await this.taskItemRepository.find({
|
|
|
- where: { taskId: task.id, status: TaskItemStatus.PENDING },
|
|
|
- order: { id: 'ASC' },
|
|
|
- take: batchTotal
|
|
|
- })
|
|
|
-
|
|
|
- if (pendingItems.length === 0) {
|
|
|
- await this.finalizeTaskIfDone(task.id)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const queue = [...pendingItems]
|
|
|
- const workerCount = Math.min(concurrentCount, queue.length)
|
|
|
-
|
|
|
- const workerResults = await Promise.allSettled(
|
|
|
- Array.from({ length: workerCount }, (_, index) => this.runSenderWorker(task, queue, sendIntervalMs, index))
|
|
|
- )
|
|
|
-
|
|
|
- let batchSent = 0
|
|
|
- let batchSuccess = 0
|
|
|
-
|
|
|
- workerResults.forEach((result, index) => {
|
|
|
- if (result.status === 'fulfilled') {
|
|
|
- batchSent += result.value.sent
|
|
|
- batchSuccess += result.value.success
|
|
|
- } else {
|
|
|
- const msg =
|
|
|
- result.reason instanceof Error
|
|
|
- ? result.reason.message
|
|
|
- : typeof result.reason === 'string'
|
|
|
- ? result.reason
|
|
|
- : '未知错误'
|
|
|
- this.app.log.error(`worker=${index} 执行失败: ${msg}`)
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- if (batchSent > 0) {
|
|
|
- await this.taskRepository.increment({ id: task.id }, 'sent', batchSent)
|
|
|
- }
|
|
|
- if (batchSuccess > 0) {
|
|
|
- await this.taskRepository.increment({ id: task.id }, 'successCount', batchSuccess)
|
|
|
- }
|
|
|
-
|
|
|
- const latest = await this.taskRepository.findOne({ where: { id: task.id } })
|
|
|
- if (!latest || latest.status !== TaskStatus.SENDING) {
|
|
|
- this.app.log.info(`任务 ${task.id} 已暂停或停止,本轮结束`)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- await this.finalizeTaskIfDone(task.id)
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 如果任务已完成,则更新任务状态为已完成
|
|
|
- * @param taskId 任务ID
|
|
|
- */
|
|
|
- private async finalizeTaskIfDone(taskId: number): Promise<void> {
|
|
|
- const pendingCount = await this.taskItemRepository.count({
|
|
|
- where: { taskId, status: TaskItemStatus.PENDING }
|
|
|
- })
|
|
|
- if (pendingCount > 0) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const successCount = await this.taskItemRepository.count({
|
|
|
- where: { taskId, status: TaskItemStatus.SUCCESS }
|
|
|
- })
|
|
|
- const failedCount = await this.taskItemRepository.count({
|
|
|
- where: { taskId, status: TaskItemStatus.FAILED }
|
|
|
- })
|
|
|
-
|
|
|
- await this.taskRepository.update(taskId, {
|
|
|
- status: TaskStatus.COMPLETED,
|
|
|
- sent: successCount + failedCount,
|
|
|
- successCount
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 运行发送工作线程
|
|
|
- * 从队列中取出任务项并发送,支持发送者轮换和会话管理
|
|
|
- * @param task 任务对象
|
|
|
- * @param queue 任务项队列
|
|
|
- * @param sendIntervalMs 发送间隔(毫秒)
|
|
|
- * @param workerIndex 工作线程索引
|
|
|
- * @returns 发送统计信息(已发送、成功、失败数量)
|
|
|
- */
|
|
|
- private async runSenderWorker(
|
|
|
- task: Task,
|
|
|
- queue: TaskItem[],
|
|
|
- sendIntervalMs: number,
|
|
|
- workerIndex: number
|
|
|
- ): Promise<{ sent: number; success: number; failed: number }> {
|
|
|
- let sent = 0
|
|
|
- let success = 0
|
|
|
- let failed = 0
|
|
|
- let sender: Sender | null = null
|
|
|
- let client: TelegramClient | null = null
|
|
|
- let senderSentInRound = 0
|
|
|
-
|
|
|
- while (true) {
|
|
|
- const taskItem = queue.shift()
|
|
|
- if (!taskItem) {
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- const stillSending = await this.isTaskSending(task.id)
|
|
|
- if (!stillSending) {
|
|
|
- this.app.log.info(`任务 ${task.id} 已暂停/停止,worker=${workerIndex} 提前结束`)
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- if (!sender || senderSentInRound >= this.currentSenderSendLimit) {
|
|
|
- await this.tgClientService.disconnectClient(client)
|
|
|
- sender = await this.pickSender()
|
|
|
- const sessionString = await this.ensureSessionString(sender)
|
|
|
- try {
|
|
|
- client = await this.tgClientService.createConnectedClient(sessionString)
|
|
|
- } catch (error) {
|
|
|
- const msg = error instanceof Error ? error.message : String(error)
|
|
|
- if (this.isSessionRevokedMessage(msg)) {
|
|
|
- await this.handleSessionRevoked(sender)
|
|
|
- sender = null
|
|
|
- client = null
|
|
|
- continue
|
|
|
- }
|
|
|
- throw error
|
|
|
- }
|
|
|
- senderSentInRound = 0
|
|
|
-
|
|
|
- const me = await client.getMe().catch(() => null)
|
|
|
- const delaySeconds = this.getRandomDelaySeconds()
|
|
|
- const displayName = `${me?.firstName ?? ''} ${me?.lastName ?? ''}`.trim() || me?.username || ''
|
|
|
- this.app.log.info(
|
|
|
- `worker=${workerIndex} ,当前登录账号: id: ${me?.id ?? sender.id} ,name: ${
|
|
|
- displayName || sender.id
|
|
|
- },延迟 ${delaySeconds}s 后开始发送`
|
|
|
- )
|
|
|
- await this.sleep(delaySeconds * 1000)
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await this.sendTaskItemWithClient(task, taskItem, sender!, client!, workerIndex)
|
|
|
- success++
|
|
|
- } catch (error) {
|
|
|
- failed++
|
|
|
- const msg = error instanceof Error ? error.message : '未知错误'
|
|
|
- if (sender && this.isSessionRevokedMessage(msg)) {
|
|
|
- await this.handleSessionRevoked(sender)
|
|
|
- await this.tgClientService.disconnectClient(client)
|
|
|
- sender = null
|
|
|
- client = null
|
|
|
- senderSentInRound = 0
|
|
|
- }
|
|
|
- this.app.log.warn(
|
|
|
- `❌ 发送失败 taskId=${task.id}, item=${taskItem.id}, sender=${
|
|
|
- sender?.id ?? '未知'
|
|
|
- }, worker=${workerIndex}, error: ${msg}`
|
|
|
- )
|
|
|
- } finally {
|
|
|
- sent++
|
|
|
- senderSentInRound++
|
|
|
- if (sender) {
|
|
|
- const used = (this.senderUsageInBatch.get(sender.id) ?? 0) + 1
|
|
|
- this.senderUsageInBatch.set(sender.id, used)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (sendIntervalMs > 0) {
|
|
|
- await this.sleep(sendIntervalMs)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- await this.tgClientService.disconnectClient(client)
|
|
|
-
|
|
|
- return { sent, success, failed }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 使用客户端发送任务项
|
|
|
- * 发送消息、清除会话、删除临时联系人,并更新任务项状态
|
|
|
- * @param task 任务对象
|
|
|
- * @param taskItem 任务项对象
|
|
|
- * @param sender 发送者对象
|
|
|
- * @param client Telegram 客户端
|
|
|
- * @param workerIndex 工作线程索引
|
|
|
- */
|
|
|
- private async sendTaskItemWithClient(
|
|
|
- task: Task,
|
|
|
- taskItem: TaskItem,
|
|
|
- sender: Sender,
|
|
|
- client: TelegramClient,
|
|
|
- workerIndex: number
|
|
|
- ): Promise<void> {
|
|
|
- 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)
|
|
|
- await this.tgClientService.clearConversation(client, targetPeer).catch(() => {})
|
|
|
- await this.tgClientService.deleteTempContact(client, (targetPeer as any).id).catch(() => {})
|
|
|
-
|
|
|
- await this.taskItemRepository.update(taskItem.id, {
|
|
|
- status: TaskItemStatus.SUCCESS,
|
|
|
- sentAt: new Date(),
|
|
|
- senderId: sender.id,
|
|
|
- errorMsg: null
|
|
|
- })
|
|
|
-
|
|
|
- await this.senderService.incrementUsageCount(sender.id)
|
|
|
- this.app.log.info(
|
|
|
- `✅ 发送成功 taskId=${task.id}, itemId=${taskItem.id}, sender=${sender.id}, worker=${workerIndex}`
|
|
|
- )
|
|
|
- } catch (error) {
|
|
|
- const msg = error instanceof Error ? error.message : '未知错误'
|
|
|
- await this.taskItemRepository.update(taskItem.id, {
|
|
|
- status: TaskItemStatus.FAILED,
|
|
|
- sentAt: new Date(),
|
|
|
- senderId: sender.id,
|
|
|
- errorMsg: msg
|
|
|
- })
|
|
|
- await this.senderService.incrementUsageCount(sender.id)
|
|
|
- throw error
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 刷新发送者缓存
|
|
|
- * 从数据库重新加载所有未删除的发送者,按最后使用时间和使用次数排序
|
|
|
- */
|
|
|
- private async refreshSenderCache(): Promise<void> {
|
|
|
- this.senderCache = await this.senderRepository.find({
|
|
|
- where: { delFlag: false },
|
|
|
- order: { lastUsageTime: 'ASC', usageCount: 'ASC' }
|
|
|
- })
|
|
|
- this.senderCursor = 0
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查任务是否正在发送中
|
|
|
- * @param taskId 任务ID
|
|
|
- * @returns 如果任务状态为发送中且未删除返回 true,否则返回 false
|
|
|
- */
|
|
|
- private async isTaskSending(taskId: number): Promise<boolean> {
|
|
|
- const current = await this.taskRepository.findOne({ where: { id: taskId } })
|
|
|
- return !!current && current.status === TaskStatus.SENDING && current.delFlag === false
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 延迟指定毫秒数
|
|
|
- * @param ms 延迟毫秒数
|
|
|
- */
|
|
|
- private async sleep(ms: number): Promise<void> {
|
|
|
- return await new Promise(resolve => setTimeout(resolve, ms))
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 选择可用的发送者
|
|
|
- * 从缓存中选择使用次数最少的发送者,如果所有发送者都达到上限则重置计数
|
|
|
- * @returns 选中的发送者对象
|
|
|
- * @throws 如果没有可用发送者则抛出错误
|
|
|
- */
|
|
|
- private async pickSender(): Promise<Sender> {
|
|
|
- if (this.senderCache.length === 0) {
|
|
|
- this.senderCache = await this.senderRepository.find({
|
|
|
- where: { delFlag: false },
|
|
|
- order: { lastUsageTime: 'ASC', usageCount: 'ASC' }
|
|
|
- })
|
|
|
- this.senderCursor = 0
|
|
|
- }
|
|
|
-
|
|
|
- if (this.senderCache.length === 0) {
|
|
|
- throw new Error('暂无可用 sender 账号')
|
|
|
- }
|
|
|
-
|
|
|
- const total = this.senderCache.length
|
|
|
- for (let i = 0; i < total; i++) {
|
|
|
- const index = (this.senderCursor + i) % total
|
|
|
- const sender = this.senderCache[index]
|
|
|
- const used = this.senderUsageInBatch.get(sender.id) ?? 0
|
|
|
- if (used < this.currentSenderSendLimit) {
|
|
|
- this.senderCursor = (index + 1) % total
|
|
|
- return sender
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.app.log.info('所有 sender 均已达到当前批次上限,重置计数后重新轮询')
|
|
|
- this.senderUsageInBatch.clear()
|
|
|
- this.senderCursor = 0
|
|
|
- return this.senderCache[0]
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 确保发送者有有效的会话字符串
|
|
|
- * 如果发送者已有 sessionStr 则直接返回,否则根据 dcId 和 authKey 构建并保存
|
|
|
- * @param sender 发送者对象
|
|
|
- * @returns 会话字符串
|
|
|
- * @throws 如果缺少必要的会话信息则抛出错误
|
|
|
- */
|
|
|
- private async ensureSessionString(sender: Sender): Promise<string> {
|
|
|
- if (sender.sessionStr) {
|
|
|
- return sender.sessionStr
|
|
|
- }
|
|
|
-
|
|
|
- if (sender.dcId && sender.authKey) {
|
|
|
- const session = buildStringSessionByDcIdAndAuthKey(sender.dcId, sender.authKey)
|
|
|
- await this.senderRepository.update(sender.id, { sessionStr: session })
|
|
|
- return session
|
|
|
- }
|
|
|
-
|
|
|
- throw new Error(`sender=${sender.id} 缺少 session 信息`)
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理会话被撤销的情况
|
|
|
- * 将发送者标记为已删除,并从缓存中移除
|
|
|
- * @param sender 发送者对象
|
|
|
- */
|
|
|
- private async handleSessionRevoked(sender: Sender): Promise<void> {
|
|
|
- await this.senderRepository.update(sender.id, { delFlag: true })
|
|
|
- this.senderCache = this.senderCache.filter(s => s.id !== sender.id)
|
|
|
- this.senderCursor = 0
|
|
|
- this.app.log.warn(`sender=${sender.id} session 失效,已删除`)
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查是否可以发送消息给目标用户
|
|
|
- * 检查用户是否被屏蔽、是否为机器人、是否已删除、是否为虚假或诈骗账号
|
|
|
- * @param client Telegram 客户端
|
|
|
- * @param targetPeer 目标用户对象
|
|
|
- * @returns 如果可以发送返回 true,否则返回 false
|
|
|
- * @throws 如果认证密钥未注册则抛出错误
|
|
|
- */
|
|
|
- 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
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取随机延迟秒数
|
|
|
- * @param min 最小延迟秒数,默认 10
|
|
|
- * @param max 最大延迟秒数,默认 20
|
|
|
- * @returns 随机延迟秒数
|
|
|
- */
|
|
|
- private getRandomDelaySeconds(min: number = 10, max: number = 20): number {
|
|
|
- return Math.floor(Math.random() * (max - min + 1)) + min
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查错误消息是否表示会话被撤销
|
|
|
- * @param msg 错误消息
|
|
|
- * @returns 如果是会话被撤销相关的错误返回 true,否则返回 false
|
|
|
- */
|
|
|
- private isSessionRevokedMessage(msg: string): boolean {
|
|
|
- return msg.includes('SESSION_REVOKED') || msg.includes('AUTH_KEY_UNREGISTERED') || msg.includes('AUTH_KEY_INVALID')
|
|
|
- }
|
|
|
}
|