Просмотр исходного кода

增强 TgClientService 的功能,添加群组管理、消息发送和连接管理相关的方法,优化错误处理和日志记录,提升代码可读性和稳定性。同时,添加详细的文档注释以便于后续维护。

wuyi 3 недель назад
Родитель
Сommit
fb42bc0df8
1 измененных файлов с 366 добавлено и 30 удалено
  1. 366 30
      src/services/tgClient.service.ts

+ 366 - 30
src/services/tgClient.service.ts

@@ -25,6 +25,12 @@ export class TgClientService {
 
   constructor() {}
 
+  // ==================== 初始化相关 ====================
+
+  /**
+   * 初始化应用配置,获取 API_ID 和 API_HASH
+   * @private
+   */
   private async initializeApp(): Promise<void> {
     if (this.initPromise) {
       return this.initPromise
@@ -50,6 +56,13 @@ export class TgClientService {
     return this.initPromise
   }
 
+  // ==================== 连接管理相关 ====================
+
+  /**
+   * 连接到 Telegram,支持 WSS 和 TCP 两种连接策略
+   * @param sessionString - Telegram Session 字符串
+   * @throws 当连接数达到上限或连接失败时抛出错误
+   */
   async connect(sessionString: string): Promise<void> {
     if (TgClientService.activeClientsCount >= TgClientService.maxActiveClients) {
       throw new Error('TelegramClient 并发连接数已达上限')
@@ -112,6 +125,9 @@ export class TgClientService {
     )
   }
 
+  /**
+   * 断开 Telegram 连接并清理资源
+   */
   async disconnect(): Promise<void> {
     if (!this.client) {
       return
@@ -121,14 +137,32 @@ export class TgClientService {
     this.client = null
   }
 
+  /**
+   * 获取当前的 TelegramClient 实例
+   * @returns TelegramClient 实例,如果未连接则返回 null
+   */
   getClient(): TelegramClient | null {
     return this.client
   }
 
+  /**
+   * 检查客户端是否已连接
+   * @returns 如果客户端存在且已连接返回 true,否则返回 false
+   */
   isConnected(): boolean {
     return this.client !== null && (this.client.connected ?? false)
   }
 
+  // ==================== 频道/群组管理相关 ====================
+
+  /**
+   * 创建频道或超级群组
+   * @param groupName - 群组名称
+   * @param groupDescription - 群组描述
+   * @param groupType - 群组类型:'megagroup' 或 'channel'
+   * @returns 返回创建的群组信息,包含 chatId 和 accessHash
+   * @throws 当创建失败时抛出错误
+   */
   async createChannelGroup(
     groupName: string,
     groupDescription: string,
@@ -172,6 +206,12 @@ export class TgClientService {
     }
   }
 
+  /**
+   * 根据 chatId 和 accessHash 创建 InputChannel 对象
+   * @param chatId - 频道/群组 ID
+   * @param accessHash - 访问哈希值
+   * @returns InputChannel 对象
+   */
   async getInputChannel(chatId: string | number, accessHash: string | number): Promise<Api.InputChannel> {
     return new Api.InputChannel({
       channelId: bigInt(chatId.toString()),
@@ -179,39 +219,155 @@ export class TgClientService {
     })
   }
 
-  async sendMessageToChannelGroup(inputChannel: Api.InputChannel, message: string): Promise<void> {
+  /**
+   * 通过邀请链接加入群组
+   * @param inviteLink - Telegram 邀请链接
+   * @returns 返回加入的群组信息,包含 chatId 和 title
+   * @throws 当邀请链接无效、过期或加入失败时抛出错误
+   */
+  async joinGroupByInviteLink(inviteLink: string): Promise<{ chatId: string; title: string }> {
     this.ensureConnected()
-    this.app.log.info('正在发送群组消息...')
+    this.app.log.info(`正在通过邀请链接加入群组: ${inviteLink}`)
 
     try {
-      await this.client!.sendMessage(inputChannel, {
-        message: message.trim()
-      })
-      this.app.log.info('已向群组发送消息')
+      // 解析邀请链接,提取 hash
+      const hash = this.extractInviteHash(inviteLink)
+      if (!hash) {
+        throw new Error('无效的邀请链接格式')
+      }
+
+      // 检查邀请链接信息
+      await this.client!.invoke(
+        new Api.messages.CheckChatInvite({
+          hash
+        })
+      )
+
+      // 使用邀请链接加入群组
+      const result = await this.client!.invoke(
+        new Api.messages.ImportChatInvite({
+          hash
+        })
+      )
+
+      const updates = result as any
+      let chat = updates.chats?.[0]
+
+      if (!chat) {
+        // 如果是 Updates 类型,尝试从 updates 中获取
+        if (updates.updates) {
+          for (const update of updates.updates) {
+            if (update.className === 'UpdateNewMessage' && update.message?.peerId) {
+              const chatId = update.message.peerId.channelId || update.message.peerId.chatId
+              if (chatId && updates.chats) {
+                chat = updates.chats.find((c: any) => c.id?.toString() === chatId?.toString())
+                break
+              }
+            }
+          }
+        }
+      }
+
+      if (!chat) {
+        throw new Error('加入群组失败,未返回群组信息')
+      }
+
+      const chatId = chat.id?.toString() || chat.id
+      const title = chat.title || '未知群组'
+
+      this.app.log.info(`成功加入群组: ${title} (ID: ${chatId})`)
+
+      return {
+        chatId: String(chatId),
+        title
+      }
     } catch (error) {
-      const errorMessage = this.extractErrorMessage(error)
-      throw new Error(`发送群组消息失败: ${errorMessage}`)
+      let errorMessage = this.extractErrorMessage(error)
+
+      if (errorMessage.includes('INVITE_HASH_EXPIRED')) {
+        errorMessage = '邀请链接已过期'
+      } else if (errorMessage.includes('INVITE_HASH_INVALID')) {
+        errorMessage = '邀请链接无效'
+      } else if (errorMessage.includes('USER_ALREADY_PARTICIPANT')) {
+        errorMessage = '您已经是该群组的成员'
+      } else if (errorMessage.includes('CHANNELS_TOO_MUCH')) {
+        errorMessage = '加入的频道/群组数量已达上限'
+      } else if (errorMessage.includes('INVITE_REQUEST_SENT')) {
+        errorMessage = '已发送加入请求,等待管理员批准'
+      }
+
+      throw new Error(`通过邀请链接加入群组失败: ${errorMessage}`)
     }
   }
 
-  async inviteMembersToChannelGroup(inputChannel: Api.InputChannel, inputUsers: Api.InputUser[]): Promise<void> {
+  /**
+   * 退出指定的频道或群组
+   * @param target - 群组标识,可以是 chatId、username 或 InputChannel 对象
+   * @returns 返回操作结果,包含 success 状态和群组标题(如果可用)
+   * @throws 当退出失败时抛出错误
+   */
+  async leaveGroup(target: string | number | Api.InputChannel): Promise<{ success: boolean; title?: string }> {
     this.ensureConnected()
-    this.app.log.info('正在邀请成员到群组...')
+    this.app.log.info(`正在退出群组: ${target}`)
 
     try {
+      let inputChannel: Api.InputChannel
+      let chatTitle: string | undefined
+
+      // 如果已经是 InputChannel,直接使用
+      if (target instanceof Api.InputChannel) {
+        inputChannel = target
+      } else {
+        // 获取群组实体
+        const entity = await this.client!.getEntity(target)
+
+        if (!entity) {
+          throw new Error('未找到该群组')
+        }
+
+        chatTitle = (entity as any).title
+        const channel = entity as any
+
+        inputChannel = new Api.InputChannel({
+          channelId: bigInt(channel.id.toString()),
+          accessHash: bigInt(channel.accessHash.toString())
+        })
+      }
+
+      // 退出频道/超级群组
       await this.client!.invoke(
-        new Api.channels.InviteToChannel({
-          channel: inputChannel,
-          users: inputUsers
+        new Api.channels.LeaveChannel({
+          channel: inputChannel
         })
       )
-      this.app.log.info('已邀请成员到群组')
+
+      this.app.log.info(`成功退出群组${chatTitle ? `: ${chatTitle}` : ''}`)
+
+      return {
+        success: true,
+        title: chatTitle
+      }
     } catch (error) {
       const errorMessage = this.extractErrorMessage(error)
-      throw new Error(`邀请成员到群组失败: ${errorMessage}`)
+      let errorMsg = `退出群组失败: ${errorMessage}`
+
+      if (errorMessage.includes('CHANNEL_PRIVATE')) {
+        errorMsg = '该群组不存在或您不是成员'
+      } else if (errorMessage.includes('USER_NOT_PARTICIPANT')) {
+        errorMsg = '您不是该群组的成员'
+      } else if (errorMessage.includes('CHAT_ADMIN_REQUIRED')) {
+        errorMsg = '需要管理员权限才能执行此操作'
+      }
+
+      throw new Error(errorMsg)
     }
   }
 
+  /**
+   * 获取群组的邀请链接
+   * @param inputChannel - InputChannel 对象
+   * @returns 返回邀请链接,如果获取失败则返回 null
+   */
   async getInviteLink(inputChannel: Api.InputChannel): Promise<string | null> {
     this.ensureConnected()
 
@@ -228,6 +384,12 @@ export class TgClientService {
     return null
   }
 
+  /**
+   * 为群组设置公开用户名并返回公开链接
+   * @param inputChannel - InputChannel 对象
+   * @param groupName - 群组名称(可选,用于生成用户名)
+   * @returns 返回公开链接,如果设置失败则返回 null
+   */
   async getPublicLink(inputChannel: Api.InputChannel, groupName?: string): Promise<string | null> {
     this.ensureConnected()
 
@@ -247,6 +409,85 @@ export class TgClientService {
     return null
   }
 
+  // ==================== 消息发送相关 ====================
+
+  /**
+   * 向频道或群组发送消息
+   * @param inputChannel - InputChannel 对象
+   * @param message - 要发送的消息内容
+   * @throws 当发送失败时抛出错误
+   */
+  async sendMessageToChannelGroup(inputChannel: Api.InputChannel, message: string): Promise<void> {
+    this.ensureConnected()
+    this.app.log.info('正在发送群组消息...')
+
+    try {
+      await this.client!.sendMessage(inputChannel, {
+        message: message.trim()
+      })
+      this.app.log.info('已向群组发送消息')
+    } catch (error) {
+      const errorMessage = this.extractErrorMessage(error)
+      throw new Error(`发送群组消息失败: ${errorMessage}`)
+    }
+  }
+
+  /**
+   * 向指定的用户或群组发送消息
+   * @param targetPeer - 目标实体(用户或群组)
+   * @param message - 要发送的消息内容
+   * @returns 返回发送结果
+   * @throws 当发送失败时抛出错误
+   */
+  async sendMessageToPeer(targetPeer: any, message: string): Promise<any> {
+    this.ensureConnected()
+    this.app.log.info('正在发送消息...')
+
+    try {
+      const result = await this.client!.sendMessage(targetPeer, {
+        message: message
+      })
+      return result
+    } catch (error) {
+      const errorMessage = this.extractErrorMessage(error)
+      throw new Error(`发送消息失败: ${errorMessage}`)
+    }
+  }
+
+  // ==================== 成员管理相关 ====================
+
+  /**
+   * 邀请成员加入频道或群组
+   * @param inputChannel - InputChannel 对象
+   * @param inputUsers - 要邀请的用户列表
+   * @throws 当邀请失败时抛出错误
+   */
+  async inviteMembersToChannelGroup(inputChannel: Api.InputChannel, inputUsers: Api.InputUser[]): Promise<void> {
+    this.ensureConnected()
+    this.app.log.info('正在邀请成员到群组...')
+
+    try {
+      await this.client!.invoke(
+        new Api.channels.InviteToChannel({
+          channel: inputChannel,
+          users: inputUsers
+        })
+      )
+      this.app.log.info('已邀请成员到群组')
+    } catch (error) {
+      const errorMessage = this.extractErrorMessage(error)
+      throw new Error(`邀请成员到群组失败: ${errorMessage}`)
+    }
+  }
+
+  // ==================== 联系人管理相关 ====================
+
+  /**
+   * 获取目标实体信息,支持通过手机号或 ID 获取
+   * @param parsedTarget - 目标标识,可以是手机号(以+开头)或用户ID/用户名
+   * @returns 返回目标实体对象
+   * @throws 当无法获取目标信息时抛出错误
+   */
   async getTargetPeer(parsedTarget: string | number): Promise<any> {
     this.ensureConnected()
     this.app.log.info('正在获取目标实体信息...')
@@ -289,21 +530,11 @@ export class TgClientService {
     }
   }
 
-  async sendMessageToPeer(targetPeer: any, message: string): Promise<any> {
-    this.ensureConnected()
-    this.app.log.info('正在发送消息...')
-
-    try {
-      const result = await this.client!.sendMessage(targetPeer, {
-        message: message
-      })
-      return result
-    } catch (error) {
-      const errorMessage = this.extractErrorMessage(error)
-      throw new Error(`发送消息失败: ${errorMessage}`)
-    }
-  }
-
+  /**
+   * 清除与指定实体的会话历史记录
+   * @param targetPeer - 目标实体(用户或群组)
+   * @throws 当清除失败时抛出错误
+   */
   async clearConversation(targetPeer: any): Promise<void> {
     this.ensureConnected()
     this.app.log.info('正在清除会话...')
@@ -321,6 +552,11 @@ export class TgClientService {
     }
   }
 
+  /**
+   * 删除临时联系人
+   * @param userId - 要删除的用户 ID
+   * @throws 当删除失败时抛出错误
+   */
   async deleteTempContact(userId: number): Promise<void> {
     this.ensureConnected()
     this.app.log.info('正在删除临时联系人...')
@@ -337,12 +573,26 @@ export class TgClientService {
     }
   }
 
+  // ==================== 私有辅助方法 ====================
+
+  /**
+   * 确保客户端已连接,如果未连接则抛出错误
+   * @private
+   * @throws 当客户端未连接时抛出错误
+   */
   private ensureConnected(): void {
     if (!this.client) {
       throw new Error('TelegramClient 未连接,请先调用 connect() 方法')
     }
   }
 
+  /**
+   * 销毁客户端并清理资源
+   * @private
+   * @param client - 要销毁的 TelegramClient 实例
+   * @param context - 上下文信息,用于日志记录
+   * @param decrementCount - 是否减少活跃客户端计数
+   */
   private async disposeClient(client: TelegramClient | null, context: string, decrementCount: boolean): Promise<void> {
     if (!client) {
       return
@@ -376,6 +626,12 @@ export class TgClientService {
     }
   }
 
+  /**
+   * 从错误对象中提取错误消息
+   * @private
+   * @param error - 错误对象
+   * @returns 错误消息字符串
+   */
   private extractErrorMessage(error: unknown): string {
     if (error instanceof Error) {
       return error.message
@@ -386,6 +642,40 @@ export class TgClientService {
     return '未知错误'
   }
 
+  /**
+   * 从邀请链接中提取 hash 值
+   * @private
+   * @param inviteLink - Telegram 邀请链接
+   * @returns 提取的 hash 值,如果解析失败则返回 null
+   */
+  private extractInviteHash(inviteLink: string): string | null {
+    try {
+      // 支持的格式:
+      // https://t.me/+ojthGOSrE4czNzhk
+      // https://t.me/joinchat/ojthGOSrE4czNzhk
+      // t.me/+ojthGOSrE4czNzhk
+      const patterns = [/t\.me\/\+([A-Za-z0-9_-]+)/, /t\.me\/joinchat\/([A-Za-z0-9_-]+)/, /^([A-Za-z0-9_-]+)$/]
+
+      for (const pattern of patterns) {
+        const match = inviteLink.match(pattern)
+        if (match && match[1]) {
+          return match[1]
+        }
+      }
+
+      return null
+    } catch (error) {
+      this.app.log.error(`解析邀请链接失败: ${this.extractErrorMessage(error)}`)
+      return null
+    }
+  }
+
+  /**
+   * 生成群组用户名
+   * @private
+   * @param groupName - 群组名称(可选)
+   * @returns 生成的用户名
+   */
   private generateGroupUsername(groupName?: string): string {
     const random = Math.random().toString(36).slice(2, 8)
     const maxLength = 32
@@ -395,6 +685,12 @@ export class TgClientService {
     return `group_${safeName}_${random}`
   }
 
+  /**
+   * 构建英文名称,如果名称不是纯英文则使用随机单词
+   * @private
+   * @param name - 原始名称(可选)
+   * @returns 处理后的英文名称
+   */
   private buildEnglishName(name?: string): string {
     if (name && this.isPureEnglish(name)) {
       const sanitized = this.sanitizeName(name)
@@ -405,10 +701,22 @@ export class TgClientService {
     return this.getRandomEnglishWord()
   }
 
+  /**
+   * 检查字符串是否为纯英文(包含字母、数字、空格、下划线和连字符)
+   * @private
+   * @param value - 要检查的字符串
+   * @returns 如果是纯英文返回 true,否则返回 false
+   */
   private isPureEnglish(value: string): boolean {
     return /^[A-Za-z0-9 _-]+$/.test(value)
   }
 
+  /**
+   * 清理名称字符串,转换为小写并替换特殊字符为下划线
+   * @private
+   * @param name - 要清理的名称
+   * @returns 清理后的名称
+   */
   private sanitizeName(name: string): string {
     return name
       .trim()
@@ -418,6 +726,11 @@ export class TgClientService {
       .replace(/_+/g, '_')
   }
 
+  /**
+   * 获取随机英文单词
+   * @private
+   * @returns 随机选择的英文单词
+   */
   private getRandomEnglishWord(): string {
     const words = [
       'apple',
@@ -439,11 +752,23 @@ export class TgClientService {
     return words[Math.floor(Math.random() * words.length)]
   }
 
+  /**
+   * 检查错误是否为 Session 被吊销的错误
+   * @private
+   * @param error - 错误对象
+   * @returns 如果是 Session 吊销错误返回 true,否则返回 false
+   */
   private isSessionRevokedError(error: unknown): boolean {
     const msg = this.extractErrorMessage(error)
     return msg.includes('SESSION_REVOKED') || msg.includes('AUTH_KEY_UNREGISTERED') || msg.includes('AUTH_KEY_INVALID')
   }
 
+  /**
+   * 带超时的连接方法
+   * @private
+   * @param client - TelegramClient 实例
+   * @throws 当连接超时或失败时抛出错误
+   */
   private async connectWithTimeout(client: TelegramClient): Promise<void> {
     let timer: NodeJS.Timeout | null = null
 
@@ -465,6 +790,13 @@ export class TgClientService {
     }
   }
 
+  /**
+   * 验证 Session 是否有效,并获取账号信息
+   * @private
+   * @param client - TelegramClient 实例
+   * @returns 返回当前登录账号信息
+   * @throws 当 Session 无效时抛出错误
+   */
   private async ensureValidSession(client: TelegramClient): Promise<any> {
     try {
       this.app.log.info('TelegramClient 连接成功,正在获取账号信息...')
@@ -477,6 +809,10 @@ export class TgClientService {
     }
   }
 
+  /**
+   * 记录当前活跃的 Telegram 客户端数量
+   * @private
+   */
   private logActiveClientCount(): void {
     this.app?.log?.info?.(`当前活跃 Telegram 客户端数量: ${TgClientService.activeClientsCount}`)
   }