Pārlūkot izejas kodu

增强 TgMsgSendController 的消息发送功能,支持通过 dcId 和 authKey 构建会话字符串,优化请求验证和错误处理,提升代码可读性和健壮性。

wuyi 1 mēnesi atpakaļ
vecāks
revīzija
0f71beccc3
3 mainītis faili ar 210 papildinājumiem un 131 dzēšanām
  1. 94 44
      src/controllers/tg-msg-send.controller.ts
  2. 2 0
      src/dto/tg-msg-send.dto.ts
  3. 114 87
      src/utils/tg.util.ts

+ 94 - 44
src/controllers/tg-msg-send.controller.ts

@@ -1,16 +1,23 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { TgMsgSendService } from '../services/tg-msg-send.service'
 import { SendTgMsgSendBody } from '../dto/tg-msg-send.dto'
-import { buildStringSession } from '../utils/tg.util'
+import { buildStringSession, buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
 
 interface ValidationError {
   field: string
   message: string
 }
 
+interface ApiResponse<T = unknown> {
+  success: boolean
+  message: string
+  data?: T
+  error?: string
+}
+
 export class TgMsgSendController {
-  private tgMsgSendService: TgMsgSendService
-  private app: FastifyInstance
+  private readonly tgMsgSendService: TgMsgSendService
+  private readonly app: FastifyInstance
 
   constructor(app: FastifyInstance) {
     this.tgMsgSendService = new TgMsgSendService(app)
@@ -19,76 +26,119 @@ export class TgMsgSendController {
 
   async sendMessage(request: FastifyRequest<{ Body: SendTgMsgSendBody }>, reply: FastifyReply) {
     try {
-      const { session, target, message } = request.body
+      const { session, target, message, dcId, authKey } = request.body
 
-      const validationError = this.validateRequest(session, target, message)
+      const validationError = this.validateRequest(session, target, message, dcId, authKey)
       if (validationError) {
-        return reply.code(400).send({
-          success: false,
-          message: validationError.message,
-          error: validationError.message
-        })
+        return this.sendErrorResponse(reply, 400, validationError.message)
       }
 
-      // 构建 session
-      const stringSession = buildStringSession(session)
+      const stringSession = this.buildSessionString(session, dcId, authKey)
       this.app.log.debug('构建 session 完成', { sessionLength: stringSession.length })
 
       const result = await this.tgMsgSendService.sendMessage(stringSession, target, message)
 
       if (result.success) {
-        return reply.code(200).send({
-          success: true,
-          message: '消息发送成功',
-          data: {
-            messageId: result.messageId
-          }
-        })
+        return this.sendSuccessResponse(reply, '消息发送成功', { messageId: result.messageId })
       } else {
-        return reply.code(500).send({
-          success: false,
-          message: '消息发送失败',
-          data: {
-            error: result.error
-          }
-        })
+        return this.sendErrorResponse(reply, 200, '消息发送失败', result.error)
       }
     } catch (error) {
-      let errorMessage = '未知错误' as string
-      if (error instanceof Error) {
-        errorMessage = error.message
+      const errorMessage = this.extractErrorMessage(error)
+      this.app.log.error('发送消息时发生未预期的错误', { error: errorMessage })
+      return this.sendErrorResponse(reply, 200, '发送消息时发生错误', errorMessage)
+    }
+  }
+
+  private buildSessionString(
+    session: string | undefined,
+    dcId: number | undefined,
+    authKey: string | undefined
+  ): string {
+    if (dcId && authKey) {
+      return buildStringSessionByDcIdAndAuthKey(dcId, authKey)
+    }
+
+    if (!session) {
+      // 这个错误理论上不应该发生,因为 validateRequest 已经检查过了
+      // 但为了代码健壮性,保留这个检查
+      if (authKey && !dcId) {
+        throw new Error('提供了 authKey 时必须同时提供 dcId')
       }
-      if (typeof error === 'string') {
-        errorMessage = error
+      if (dcId && !authKey) {
+        throw new Error('提供了 dcId 时必须同时提供 authKey')
       }
-      this.app.log.error('发送消息时发生未预期的错误', { error: errorMessage })
-      return reply.code(500).send({
-        success: false,
-        message: '发送消息时发生错误',
-        data: {
-          error: errorMessage
-        }
-      })
+      throw new Error('session 参数不能为空,或者必须同时提供 dcId 和 authKey')
     }
+
+    return buildStringSession(session)
   }
 
   private validateRequest(
     session: string | undefined,
     target: string | undefined,
-    message: string | undefined
+    message: string | undefined,
+    dcId: number | undefined,
+    authKey: string | undefined
   ): ValidationError | null {
-    if (!session || session.trim().length === 0) {
-      return { field: 'session', message: 'Session 不能为空' }
+    const hasSession = session && session.trim().length > 0
+    const hasDcIdAndAuthKey = dcId && authKey && authKey.trim().length > 0
+
+    if (!hasSession && !hasDcIdAndAuthKey) {
+      if (!dcId || !authKey) {
+        return {
+          field: 'dcId,authKey',
+          message: 'dcId 和 authKey 不能为空'
+        }
+      }
+      return {
+        field: 'session',
+        message: 'session 不能为空'
+      }
     }
 
     if (!target || target.trim().length === 0) {
-      return { field: 'target', message: '目标用户 (target) 不能为空' }
+      return {
+        field: 'target',
+        message: '目标用户 (target) 不能为空'
+      }
     }
 
     if (!message || message.trim().length === 0) {
-      return { field: 'message', message: '消息内容不能为空' }
+      return {
+        field: 'message',
+        message: '消息内容不能为空'
+      }
     }
 
     return null
   }
+
+  private extractErrorMessage(error: unknown): string {
+    if (error instanceof Error) {
+      return error.message
+    }
+    if (typeof error === 'string') {
+      return error
+    }
+    return '未知错误'
+  }
+
+  private sendSuccessResponse<T>(reply: FastifyReply, message: string, data?: T): FastifyReply {
+    const response: ApiResponse<T> = {
+      success: true,
+      message,
+      ...(data && { data })
+    }
+    return reply.code(200).send(response)
+  }
+
+  private sendErrorResponse(reply: FastifyReply, statusCode: number, message: string, error?: string): FastifyReply {
+    const response: ApiResponse = {
+      success: false,
+      message,
+      error
+    }
+    return reply.code(statusCode).send(response)
+  }
 }

+ 2 - 0
src/dto/tg-msg-send.dto.ts

@@ -2,6 +2,8 @@ export interface SendTgMsgSendBody {
   session: string
   target: string
   message: string
+  dcId?: number
+  authKey?: string
 }
 
 export interface SendMessageResult {

+ 114 - 87
src/utils/tg.util.ts

@@ -12,6 +12,11 @@ const DC_CONFIGS: Record<number, DCConfig> = {
   5: { id: 5, host: '149.154.171.5', port: 80 }
 }
 
+const AUTH_KEY_BYTE_LENGTH = 256
+const SESSION_VERSION_PREFIX = '1'
+const MIN_DC_ID = 1
+const MAX_DC_ID = 5
+
 export interface TelegramAccountSession {
   dc1_auth_key?: string
   dc1_server_salt?: string
@@ -42,20 +47,21 @@ export interface DecodedSessionData {
 }
 
 export function decodeSession(sessionStr: string): DecodedSessionData {
-  let decodedSessionData: DecodedSessionData
-
   try {
     const decodedString = Buffer.from(sessionStr, 'base64').toString('utf-8')
-    decodedSessionData = JSON.parse(decodedString) as DecodedSessionData
-  } catch (e) {
-    throw new Error('数据格式无效')
-  }
+    const decodedSessionData = JSON.parse(decodedString) as DecodedSessionData
 
-  if (!decodedSessionData || typeof decodedSessionData !== 'object') {
-    throw new Error('解析数据失败')
-  }
+    if (!decodedSessionData || typeof decodedSessionData !== 'object') {
+      throw new Error('解析数据失败:数据不是有效的对象')
+    }
 
-  return decodedSessionData
+    return decodedSessionData
+  } catch (error) {
+    if (error instanceof SyntaxError) {
+      throw new Error('数据格式无效:JSON 解析失败')
+    }
+    throw new Error(`数据格式无效:${(error as Error).message}`)
+  }
 }
 
 export function buildStringSession(sessionStr: string): string {
@@ -68,83 +74,96 @@ export function buildStringSession(sessionStr: string): string {
     }
 
     const { account, dcId } = accountInfo
+    const authKeyHex = getAuthKeyByDcId(account, dcId)
+    const authKeyBuffer = validateAndConvertAuthKey(authKeyHex)
+    const sessionBuffer = buildSessionBuffer(dcId, authKeyBuffer)
+    const encodedSession = sessionBuffer.toString('base64')
+
+    return SESSION_VERSION_PREFIX + encodedSession
+  } catch (error) {
+    throw new Error(`构建 StringSession 失败: ${(error as Error).message}`)
+  }
+}
 
-    let authKeyHex: string | undefined
-
-    switch (dcId) {
-      case 1:
-        authKeyHex = account.dc1_auth_key
-        break
-      case 2:
-        authKeyHex = account.dc2_auth_key
-        break
-      case 3:
-        authKeyHex = account.dc3_auth_key
-        break
-      case 4:
-        authKeyHex = account.dc4_auth_key
-        break
-      case 5:
-        authKeyHex = account.dc5_auth_key
-        break
-      default:
-        throw new Error(`不支持的 DC ID: ${dcId}`)
+export function buildStringSessionByDcIdAndAuthKey(dcId: number, authKey: string): string {
+  try {
+    if (dcId < MIN_DC_ID || dcId > MAX_DC_ID) {
+      throw new Error(`不支持的 DC ID: ${dcId},必须是 ${MIN_DC_ID}-${MAX_DC_ID} 之间的值`)
     }
 
-    if (!authKeyHex) {
-      throw new Error(`DC ${dcId} 的 auth_key 不存在`)
+    if (!authKey || typeof authKey !== 'string' || authKey.trim().length === 0) {
+      throw new Error('auth_key 不能为空')
     }
 
-    const authKeyBuffer = Buffer.from(authKeyHex, 'hex')
+    const authKeyBuffer = validateAndConvertAuthKey(authKey)
+    const sessionBuffer = buildSessionBuffer(dcId, authKeyBuffer)
+    const encodedSession = sessionBuffer.toString('base64')
 
-    if (authKeyBuffer.length !== 256) {
-      throw new Error(`Auth Key 长度不正确,期望 256 字节,实际 ${authKeyBuffer.length} 字节`)
-    }
+    return SESSION_VERSION_PREFIX + encodedSession
+  } catch (error) {
+    throw new Error(`构建 StringSession 失败: ${(error as Error).message}`)
+  }
+}
 
-    const dcConfig = DC_CONFIGS[dcId]
-    if (!dcConfig) {
-      throw new Error(`不支持的 DC ID: ${dcId}`)
-    }
+function getAuthKeyByDcId(account: TelegramAccountSession, dcId: number): string {
+  const authKeyMap: Record<number, keyof TelegramAccountSession> = {
+    1: 'dc1_auth_key',
+    2: 'dc2_auth_key',
+    3: 'dc3_auth_key',
+    4: 'dc4_auth_key',
+    5: 'dc5_auth_key'
+  }
 
-    const buffers: Buffer[] = []
+  const authKeyField = authKeyMap[dcId]
+  if (!authKeyField) {
+    throw new Error(`不支持的 DC ID: ${dcId}`)
+  }
 
-    buffers.push(Buffer.from([dcId]))
+  const authKeyHex = account[authKeyField] as string | undefined
+  if (!authKeyHex) {
+    throw new Error(`DC ${dcId} 的 auth_key 不存在`)
+  }
 
-    const serverAddressBuffer = Buffer.from(dcConfig.host)
-    const addressLengthBuffer = Buffer.allocUnsafe(2)
-    addressLengthBuffer.writeInt16BE(serverAddressBuffer.length, 0)
-    buffers.push(addressLengthBuffer)
+  return authKeyHex
+}
 
-    buffers.push(serverAddressBuffer)
+function validateAndConvertAuthKey(authKeyHex: string): Buffer {
+  const authKeyBuffer = Buffer.from(authKeyHex, 'hex')
 
-    const portBuffer = Buffer.allocUnsafe(2)
-    portBuffer.writeInt16BE(dcConfig.port, 0)
-    buffers.push(portBuffer)
+  if (authKeyBuffer.length !== AUTH_KEY_BYTE_LENGTH) {
+    throw new Error(`Auth Key 长度不正确,期望 ${AUTH_KEY_BYTE_LENGTH} 字节,实际 ${authKeyBuffer.length} 字节`)
+  }
 
-    buffers.push(authKeyBuffer)
+  return authKeyBuffer
+}
 
-    const sessionBuffer = Buffer.concat(buffers)
+function buildSessionBuffer(dcId: number, authKeyBuffer: Buffer): Buffer {
+  const dcConfig = DC_CONFIGS[dcId]
+  if (!dcConfig) {
+    throw new Error(`不支持的 DC ID: ${dcId}`)
+  }
 
-    const encodedSession = sessionBuffer.toString('base64')
+  const buffers: Buffer[] = [
+    Buffer.from([dcId]),
+    Buffer.allocUnsafe(2),
+    Buffer.from(dcConfig.host),
+    Buffer.allocUnsafe(2),
+    authKeyBuffer
+  ]
 
-    const sessionString = '1' + encodedSession
+  buffers[1].writeInt16BE(buffers[2].length, 0)
+  buffers[3].writeInt16BE(dcConfig.port, 0)
 
-    return sessionString
-  } catch (error) {
-    throw new Error(`构建 StringSession 失败: ${(error as Error).message}`)
-  }
+  return Buffer.concat(buffers)
 }
 
 function extractAccountData(decodedData: DecodedSessionData): { account: TelegramAccountSession; dcId: number } | null {
-  const specialKeys = ['auth_key_fingerprint', 'user_auth', 'dc']
+  const specialKeys: (keyof DecodedSessionData)[] = ['auth_key_fingerprint', 'user_auth', 'dc']
 
-  let dcId: number | undefined = decodedData.dc as number
+  let dcId: number | undefined = decodedData.dc as number | undefined
   if (!dcId && decodedData.user_auth) {
-    const userAuth = decodedData.user_auth as any
-    dcId = userAuth.dcID
-    if (dcId === 0) {
-      dcId = undefined
-    }
+    const userAuth = decodedData.user_auth as UserAuth
+    dcId = userAuth.dcID && userAuth.dcID !== 0 ? userAuth.dcID : undefined
   }
 
   for (const key in decodedData) {
@@ -153,30 +172,18 @@ function extractAccountData(decodedData: DecodedSessionData): { account: Telegra
     }
 
     const value = decodedData[key]
-    if (value && typeof value === 'object') {
-      const account = value as any
-
-      if (account.userId || account.dc1_auth_key || account.dc2_auth_key || account.dc5_auth_key) {
-        const accountDcId = account.dcId || dcId
-
-        if (!accountDcId) {
-          if (account.dc5_auth_key) {
-            dcId = 5
-          } else if (account.dc2_auth_key) {
-            dcId = 2
-          } else if (account.dc1_auth_key) {
-            dcId = 1
-          } else if (account.dc3_auth_key) {
-            dcId = 3
-          } else if (account.dc4_auth_key) {
-            dcId = 4
-          }
-        } else {
-          dcId = accountDcId
+    if (value && typeof value === 'object' && !Array.isArray(value)) {
+      const account = value as Partial<TelegramAccountSession>
+
+      if (isValidAccountObject(account)) {
+        let finalDcId = (account.dcId as number | undefined) || dcId
+
+        if (!finalDcId) {
+          finalDcId = inferDcIdFromAuthKeys(account)
         }
 
-        if (dcId) {
-          return { account: account as TelegramAccountSession, dcId }
+        if (finalDcId && finalDcId >= MIN_DC_ID && finalDcId <= MAX_DC_ID) {
+          return { account: account as TelegramAccountSession, dcId: finalDcId }
         }
       }
     }
@@ -184,3 +191,23 @@ function extractAccountData(decodedData: DecodedSessionData): { account: Telegra
 
   return null
 }
+
+function isValidAccountObject(obj: Partial<TelegramAccountSession>): boolean {
+  return !!(
+    obj.userId ||
+    obj.dc1_auth_key ||
+    obj.dc2_auth_key ||
+    obj.dc3_auth_key ||
+    obj.dc4_auth_key ||
+    obj.dc5_auth_key
+  )
+}
+
+function inferDcIdFromAuthKeys(account: Partial<TelegramAccountSession>): number | undefined {
+  if (account.dc5_auth_key) return 5
+  if (account.dc2_auth_key) return 2
+  if (account.dc1_auth_key) return 1
+  if (account.dc3_auth_key) return 3
+  if (account.dc4_auth_key) return 4
+  return undefined
+}