|
@@ -94,9 +94,7 @@ export class TgClientService {
|
|
|
for (const strategy of this.connectionStrategies) {
|
|
for (const strategy of this.connectionStrategies) {
|
|
|
const connectionOptions = { ...this.defaultConnectionOptions, ...strategy.options }
|
|
const connectionOptions = { ...this.defaultConnectionOptions, ...strategy.options }
|
|
|
this.app.log.info(
|
|
this.app.log.info(
|
|
|
- `正在建立连接[${strategy.label}]... useWSS=${connectionOptions.useWSS ? 'true' : 'false'} (443 优先), retries=${
|
|
|
|
|
- connectionOptions.connectionRetries
|
|
|
|
|
- }, retryDelay=${connectionOptions.retryDelay}ms`
|
|
|
|
|
|
|
+ `正在建立连接[${strategy.label}]... retries=${connectionOptions.connectionRetries}, retryDelay=${connectionOptions.retryDelay}ms`
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const client = new TelegramClient(stringSession, this.apiId, this.apiHash, connectionOptions)
|
|
const client = new TelegramClient(stringSession, this.apiId, this.apiHash, connectionOptions)
|
|
@@ -107,7 +105,9 @@ export class TgClientService {
|
|
|
const me = await this.ensureValidSession(client)
|
|
const me = await this.ensureValidSession(client)
|
|
|
if (me) {
|
|
if (me) {
|
|
|
this.app.log.info(
|
|
this.app.log.info(
|
|
|
- `当前登录账号: id: ${me.id} ,name: ${`${me.firstName || ''} ${me.lastName || ''}`.trim()} ${me.username || ''}`.trim()
|
|
|
|
|
|
|
+ `当前登录账号: id: ${me.id} ,name: ${`${me.firstName || ''} ${me.lastName || ''}`.trim()} ${
|
|
|
|
|
+ me.username || ''
|
|
|
|
|
+ }`.trim()
|
|
|
)
|
|
)
|
|
|
} else {
|
|
} else {
|
|
|
this.app.log.warn('无法获取账号信息')
|
|
this.app.log.warn('无法获取账号信息')
|
|
@@ -146,12 +146,107 @@ export class TgClientService {
|
|
|
await this.disposeClient(client, 'TelegramClient')
|
|
await this.disposeClient(client, 'TelegramClient')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- getActiveClientCount(): number {
|
|
|
|
|
- return this.activeClients.size
|
|
|
|
|
|
|
+ async createChannelGroup(
|
|
|
|
|
+ client: TelegramClient,
|
|
|
|
|
+ groupName: string,
|
|
|
|
|
+ groupDescription: string,
|
|
|
|
|
+ groupType: 'megagroup' | 'channel'
|
|
|
|
|
+ ): Promise<{ chatId: string; accessHash: string; client: 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(`开始创建${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) {
|
|
|
|
|
+ throw new Error('创建群组失败,未返回群组信息')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const chatId = createdChat.id?.toString() || createdChat.id
|
|
|
|
|
+ const accessHash = createdChat.accessHash?.toString() || createdChat.accessHash
|
|
|
|
|
+
|
|
|
|
|
+ if (!chatId || accessHash === undefined || accessHash === null) {
|
|
|
|
|
+ throw new Error('创建群组失败,缺少 chatId 或 accessHash')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.app.log.info(`群组创建成功,ID: ${chatId}, accessHash: ${accessHash}`)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ chatId: String(chatId),
|
|
|
|
|
+ accessHash: String(accessHash),
|
|
|
|
|
+ client
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private logActiveClientCount(): void {
|
|
|
|
|
- this.app?.log?.info?.(`当前活跃 Telegram 客户端数量: ${this.activeClients.size}`)
|
|
|
|
|
|
|
+ async getInputChannel(chatId: string | number, accessHash: string | number): Promise<Api.InputChannel> {
|
|
|
|
|
+ return new Api.InputChannel({
|
|
|
|
|
+ channelId: bigInt(chatId.toString()),
|
|
|
|
|
+ accessHash: bigInt(accessHash.toString())
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async sendMessageToChannelGroup(
|
|
|
|
|
+ client: TelegramClient,
|
|
|
|
|
+ inputChannel: Api.InputChannel,
|
|
|
|
|
+ message: string
|
|
|
|
|
+ ): Promise<void> {
|
|
|
|
|
+ this.app.log.info('正在发送群组消息...')
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await client.sendMessage(inputChannel, {
|
|
|
|
|
+ message: message.trim()
|
|
|
|
|
+ })
|
|
|
|
|
+ this.app.log.info('已向群组发送消息')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
+ throw new Error(`发送群组消息失败: ${errorMessage}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getInviteLink(client: TelegramClient, inputChannel: Api.InputChannel): Promise<string | null> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const inviteLink = await client.invoke(new Api.messages.ExportChatInvite({ peer: inputChannel }))
|
|
|
|
|
+ const invite = inviteLink as any
|
|
|
|
|
+ if (invite?.link) {
|
|
|
|
|
+ return invite.link
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
+ this.app.log.error(`获取群组邀请链接失败: ${errorMessage}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getPublicLink(
|
|
|
|
|
+ client: TelegramClient,
|
|
|
|
|
+ inputChannel: Api.InputChannel,
|
|
|
|
|
+ groupName?: string
|
|
|
|
|
+ ): Promise<string | null> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const username = this.generateGroupUsername(groupName)
|
|
|
|
|
+ await client.invoke(
|
|
|
|
|
+ new Api.channels.UpdateUsername({
|
|
|
|
|
+ channel: inputChannel,
|
|
|
|
|
+ username
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ return `https://t.me/${username}`
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
+ this.app.log.error(`获取群组公开链接失败: ${errorMessage}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async getTargetPeer(client: TelegramClient, parsedTarget: string | number): Promise<any> {
|
|
async getTargetPeer(client: TelegramClient, parsedTarget: string | number): Promise<any> {
|
|
@@ -252,6 +347,37 @@ export class TgClientService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private async disposeClient(client: TelegramClient | null, context: string): Promise<void> {
|
|
|
|
|
+ if (!client) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (client.connected) {
|
|
|
|
|
+ await client.disconnect()
|
|
|
|
|
+ this.app.log.info(`${context} 已断开连接`)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
+ if (!errorMessage.includes('TIMEOUT')) {
|
|
|
|
|
+ this.app.log.error({ msg: `${context} 断开时发生错误`, error: errorMessage })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await client.destroy()
|
|
|
|
|
+ this.app.log.info(`${context} 已销毁`)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
+ if (!errorMessage.includes('TIMEOUT')) {
|
|
|
|
|
+ this.app.log.error({ msg: `${context} 销毁时发生错误`, error: errorMessage })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.activeClients.delete(client)
|
|
|
|
|
+ this.logActiveClientCount()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private extractErrorMessage(error: unknown): string {
|
|
private extractErrorMessage(error: unknown): string {
|
|
|
if (error instanceof Error) {
|
|
if (error instanceof Error) {
|
|
|
return error.message
|
|
return error.message
|
|
@@ -262,13 +388,62 @@ export class TgClientService {
|
|
|
return '未知错误'
|
|
return '未知错误'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private generateGroupUsername(groupName?: string): string {
|
|
|
|
|
+ const random = Math.random().toString(36).slice(2, 8)
|
|
|
|
|
+ const maxLength = 32
|
|
|
|
|
+ const baseName = this.buildEnglishName(groupName)
|
|
|
|
|
+ const availableLength = Math.max(1, maxLength - 'group_'.length - random.length - 1)
|
|
|
|
|
+ const safeName = baseName.slice(0, availableLength)
|
|
|
|
|
+ return `group_${safeName}_${random}`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private buildEnglishName(name?: string): string {
|
|
|
|
|
+ if (name && this.isPureEnglish(name)) {
|
|
|
|
|
+ const sanitized = this.sanitizeName(name)
|
|
|
|
|
+ if (sanitized.length > 0) {
|
|
|
|
|
+ return sanitized
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.getRandomEnglishWord()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private isPureEnglish(value: string): boolean {
|
|
|
|
|
+ return /^[A-Za-z0-9 _-]+$/.test(value)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sanitizeName(name: string): string {
|
|
|
|
|
+ return name
|
|
|
|
|
+ .trim()
|
|
|
|
|
+ .toLowerCase()
|
|
|
|
|
+ .replace(/[^a-z0-9]+/g, '_')
|
|
|
|
|
+ .replace(/^_+|_+$/g, '')
|
|
|
|
|
+ .replace(/_+/g, '_')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getRandomEnglishWord(): string {
|
|
|
|
|
+ const words = [
|
|
|
|
|
+ 'apple',
|
|
|
|
|
+ 'bridge',
|
|
|
|
|
+ 'cloud',
|
|
|
|
|
+ 'forest',
|
|
|
|
|
+ 'garden',
|
|
|
|
|
+ 'lamp',
|
|
|
|
|
+ 'ocean',
|
|
|
|
|
+ 'paper',
|
|
|
|
|
+ 'planet',
|
|
|
|
|
+ 'rocket',
|
|
|
|
|
+ 'stone',
|
|
|
|
|
+ 'stream',
|
|
|
|
|
+ 'sunrise',
|
|
|
|
|
+ 'valley',
|
|
|
|
|
+ 'wind'
|
|
|
|
|
+ ]
|
|
|
|
|
+ return words[Math.floor(Math.random() * words.length)]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private isSessionRevokedError(error: unknown): boolean {
|
|
private isSessionRevokedError(error: unknown): boolean {
|
|
|
const msg = this.extractErrorMessage(error)
|
|
const msg = this.extractErrorMessage(error)
|
|
|
- return (
|
|
|
|
|
- msg.includes('SESSION_REVOKED') ||
|
|
|
|
|
- msg.includes('AUTH_KEY_UNREGISTERED') ||
|
|
|
|
|
- msg.includes('AUTH_KEY_INVALID')
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ return msg.includes('SESSION_REVOKED') || msg.includes('AUTH_KEY_UNREGISTERED') || msg.includes('AUTH_KEY_INVALID')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async connectWithTimeout(client: TelegramClient): Promise<void> {
|
|
private async connectWithTimeout(client: TelegramClient): Promise<void> {
|
|
@@ -301,40 +476,7 @@ export class TgClientService {
|
|
|
throw new Error('Telegram Session 已失效或被吊销,需要重新登录获取新的 session')
|
|
throw new Error('Telegram Session 已失效或被吊销,需要重新登录获取新的 session')
|
|
|
}
|
|
}
|
|
|
throw error
|
|
throw error
|
|
|
- } finally {
|
|
|
|
|
- this.app.log.info('账号信息获取完成')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async disposeClient(client: TelegramClient | null, context: string): Promise<void> {
|
|
|
|
|
- if (!client) {
|
|
|
|
|
- return
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- if (client.connected) {
|
|
|
|
|
- await client.disconnect()
|
|
|
|
|
- this.app.log.info(`${context} 已断开连接`)
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
- if (!errorMessage.includes('TIMEOUT')) {
|
|
|
|
|
- this.app.log.error({ msg: `${context} 断开时发生错误`, error: errorMessage })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- await client.destroy()
|
|
|
|
|
- this.app.log.info(`${context} 已销毁`)
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- const errorMessage = this.extractErrorMessage(error)
|
|
|
|
|
- if (!errorMessage.includes('TIMEOUT')) {
|
|
|
|
|
- this.app.log.error({ msg: `${context} 销毁时发生错误`, error: errorMessage })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this.activeClients.delete(client)
|
|
|
|
|
- this.logActiveClientCount()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private logTargetInfo(targetPeer: any): void {
|
|
private logTargetInfo(targetPeer: any): void {
|
|
@@ -360,6 +502,10 @@ export class TgClientService {
|
|
|
this.app.log.info(logData)
|
|
this.app.log.info(logData)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private logActiveClientCount(): void {
|
|
|
|
|
+ this.app?.log?.info?.(`当前活跃 Telegram 客户端数量: ${this.activeClients.size}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
getClient(): TelegramClient | null {
|
|
getClient(): TelegramClient | null {
|
|
|
return this.client
|
|
return this.client
|
|
|
}
|
|
}
|