From ccbb2aebed98ad0ef48d20f1680507fd3162a1ca Mon Sep 17 00:00:00 2001 From: meoacgx Date: Fri, 19 Dec 2025 14:29:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B4=A6=E5=8F=B7=E8=AF=8A?= =?UTF-8?q?=E6=96=AD/=E7=B3=BB=E7=BB=9F=E9=80=9A=E7=9F=A5/=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E8=AE=BE=E5=A4=87=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/TelegramAccountStatusResult.cs | 61 ++++ .../Models/TelegramAuthorizationInfo.cs | 32 ++ .../Models/TelegramSystemMessage.cs | 11 + .../ServiceCollectionExtensions.cs | 1 + .../Telegram/AccountTelegramToolsService.cs | 336 ++++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 src/TelegramPanel.Core/Models/TelegramAccountStatusResult.cs create mode 100644 src/TelegramPanel.Core/Models/TelegramAuthorizationInfo.cs create mode 100644 src/TelegramPanel.Core/Models/TelegramSystemMessage.cs create mode 100644 src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs diff --git a/src/TelegramPanel.Core/Models/TelegramAccountStatusResult.cs b/src/TelegramPanel.Core/Models/TelegramAccountStatusResult.cs new file mode 100644 index 0000000..355090b --- /dev/null +++ b/src/TelegramPanel.Core/Models/TelegramAccountStatusResult.cs @@ -0,0 +1,61 @@ +using TelegramPanel.Data.Entities; + +namespace TelegramPanel.Core.Models; + +/// +/// 账号状态检测结果(用于诊断账号是否可正常连通 Telegram) +/// +public record TelegramAccountStatusResult( + bool Ok, + string Summary, + string? Details, + DateTime CheckedAtUtc, + TelegramAccountProfile? Profile = null +); + +/// +/// Telegram 账号资料快照 +/// +public record TelegramAccountProfile( + long UserId, + string? Phone, + string? Username, + string? FirstName, + string? LastName, + bool IsDeleted, + bool IsScam, + bool IsFake, + bool IsRestricted, + bool IsVerified, + bool IsPremium +) +{ + public string DisplayName + { + get + { + var full = $"{FirstName} {LastName}".Trim(); + if (!string.IsNullOrWhiteSpace(full)) + return full; + if (!string.IsNullOrWhiteSpace(Username)) + return $"@{Username}"; + if (!string.IsNullOrWhiteSpace(Phone)) + return Phone; + return UserId.ToString(); + } + } + + public void ApplyTo(Account account) + { + if (account.UserId <= 0 && UserId > 0) + account.UserId = UserId; + + if (!string.IsNullOrWhiteSpace(Username)) + account.Username = Username; + + var nickname = DisplayName; + if (!string.IsNullOrWhiteSpace(nickname)) + account.Nickname = nickname; + } +} + diff --git a/src/TelegramPanel.Core/Models/TelegramAuthorizationInfo.cs b/src/TelegramPanel.Core/Models/TelegramAuthorizationInfo.cs new file mode 100644 index 0000000..7cc3957 --- /dev/null +++ b/src/TelegramPanel.Core/Models/TelegramAuthorizationInfo.cs @@ -0,0 +1,32 @@ +namespace TelegramPanel.Core.Models; + +/// +/// 在线设备 / 会话信息(来自 account.getAuthorizations) +/// +public record TelegramAuthorizationInfo( + long Hash, + bool Current, + int ApiId, + string? AppName, + string? AppVersion, + string? DeviceModel, + string? Platform, + string? SystemVersion, + string? Ip, + string? Country, + string? Region, + DateTime? CreatedAtUtc, + DateTime? LastActiveAtUtc +) +{ + public string Title + { + get + { + var app = string.IsNullOrWhiteSpace(AppName) ? "UnknownApp" : AppName; + var device = string.IsNullOrWhiteSpace(DeviceModel) ? "UnknownDevice" : DeviceModel; + return $"{app} - {device}"; + } + } +} + diff --git a/src/TelegramPanel.Core/Models/TelegramSystemMessage.cs b/src/TelegramPanel.Core/Models/TelegramSystemMessage.cs new file mode 100644 index 0000000..6593131 --- /dev/null +++ b/src/TelegramPanel.Core/Models/TelegramSystemMessage.cs @@ -0,0 +1,11 @@ +namespace TelegramPanel.Core.Models; + +/// +/// Telegram 系统通知(常用于接收登录验证码,来自 777000 对话) +/// +public record TelegramSystemMessage( + int Id, + DateTime? DateUtc, + string Text +); + diff --git a/src/TelegramPanel.Core/ServiceCollectionExtensions.cs b/src/TelegramPanel.Core/ServiceCollectionExtensions.cs index 0239835..3428514 100644 --- a/src/TelegramPanel.Core/ServiceCollectionExtensions.cs +++ b/src/TelegramPanel.Core/ServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // 注册账号导入协调服务 services.AddScoped(); diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs new file mode 100644 index 0000000..52ae355 --- /dev/null +++ b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs @@ -0,0 +1,336 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TelegramPanel.Core.Interfaces; +using TelegramPanel.Core.Models; +using TelegramPanel.Data.Entities; +using TL; +using WTelegram; + +namespace TelegramPanel.Core.Services.Telegram; + +/// +/// 账号诊断 / 系统通知 / 在线设备管理 +/// +public class AccountTelegramToolsService +{ + private const long TelegramSystemUserId = 777000; + + private readonly AccountManagementService _accountManagement; + private readonly ITelegramClientPool _clientPool; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public AccountTelegramToolsService( + AccountManagementService accountManagement, + ITelegramClientPool clientPool, + IConfiguration configuration, + ILogger logger) + { + _accountManagement = accountManagement; + _clientPool = clientPool; + _configuration = configuration; + _logger = logger; + } + + public async Task RefreshAccountStatusAsync(int accountId) + { + var checkedAt = DateTime.UtcNow; + try + { + var client = await GetOrCreateConnectedClientAsync(accountId); + var users = await client.Users_GetUsers(InputUser.Self); + var self = users.OfType().FirstOrDefault(); + + if (self == null) + { + return new TelegramAccountStatusResult( + Ok: false, + Summary: "无法获取账号资料", + Details: "Users_GetUsers(Self) 未返回 User", + CheckedAtUtc: checkedAt); + } + + var profile = new TelegramAccountProfile( + UserId: self.id, + Phone: self.phone, + Username: self.MainUsername, + FirstName: self.first_name, + LastName: self.last_name, + IsDeleted: self.flags.HasFlag(User.Flags.deleted), + IsScam: self.flags.HasFlag(User.Flags.scam), + IsFake: self.flags.HasFlag(User.Flags.fake), + IsRestricted: self.flags.HasFlag(User.Flags.restricted), + IsVerified: self.flags.HasFlag(User.Flags.verified), + IsPremium: self.flags.HasFlag(User.Flags.premium) + ); + + var account = await _accountManagement.GetAccountAsync(accountId); + if (account != null) + { + profile.ApplyTo(account); + await _accountManagement.UpdateAccountAsync(account); + } + + var summary = "正常"; + if (profile.IsDeleted) + summary = "账号已注销/被删除"; + else if (profile.IsRestricted) + summary = "账号受限(Restricted)"; + + return new TelegramAccountStatusResult( + Ok: true, + Summary: summary, + Details: BuildProfileDetails(profile), + CheckedAtUtc: checkedAt, + Profile: profile); + } + catch (Exception ex) + { + var (summary, details) = MapTelegramException(ex); + _logger.LogWarning(ex, "RefreshAccountStatus failed for account {AccountId}", accountId); + return new TelegramAccountStatusResult( + Ok: false, + Summary: summary, + Details: details, + CheckedAtUtc: checkedAt); + } + } + + public async Task> GetLatestSystemMessagesAsync(int accountId, int limit = 20) + { + if (limit <= 0) limit = 20; + if (limit > 100) limit = 100; + + var client = await GetOrCreateConnectedClientAsync(accountId); + var peer = await TryResolveSystemPeerAsync(client); + if (peer == null) + return Array.Empty(); + + var history = await client.Messages_GetHistory(peer, limit: limit); + var list = new List(history.Messages.Length); + foreach (var msgBase in history.Messages) + { + if (msgBase is not Message m) + continue; + + var text = m.message ?? string.Empty; + if (string.IsNullOrWhiteSpace(text)) + continue; + + list.Add(new TelegramSystemMessage( + Id: m.id, + DateUtc: m.Date.ToUniversalTime(), + Text: text.Trim() + )); + } + + return list + .OrderByDescending(x => x.DateUtc ?? DateTime.MinValue) + .Take(limit) + .ToList(); + } + + public async Task> GetAuthorizationsAsync(int accountId) + { + var client = await GetOrCreateConnectedClientAsync(accountId); + var auths = await client.Account_GetAuthorizations(); + + var list = new List(auths.authorizations.Length); + foreach (var a in auths.authorizations) + { + list.Add(new TelegramAuthorizationInfo( + Hash: a.hash, + Current: a.flags.HasFlag(Authorization.Flags.current), + ApiId: a.api_id, + AppName: a.app_name, + AppVersion: a.app_version, + DeviceModel: a.device_model, + Platform: a.platform, + SystemVersion: a.system_version, + Ip: a.ip, + Country: a.country, + Region: a.region, + CreatedAtUtc: a.date_created == default ? null : a.date_created.ToUniversalTime(), + LastActiveAtUtc: a.date_active == default ? null : a.date_active.ToUniversalTime() + )); + } + + return list + .OrderByDescending(x => x.Current) + .ThenByDescending(x => x.LastActiveAtUtc ?? DateTime.MinValue) + .ToList(); + } + + public async Task KickAuthorizationAsync(int accountId, long authorizationHash) + { + var client = await GetOrCreateConnectedClientAsync(accountId); + var ok = await client.Account_ResetAuthorization(authorizationHash); + return ok; + } + + public async Task KickAllOtherAuthorizationsAsync(int accountId) + { + var client = await GetOrCreateConnectedClientAsync(accountId); + var ok = await client.Auth_ResetAuthorizations(); + return ok; + } + + private async Task TryResolveSystemPeerAsync(Client client) + { + try + { + var dialogs = await client.Messages_GetAllDialogs(); + if (!dialogs.users.TryGetValue(TelegramSystemUserId, out var userBase)) + return null; + + if (userBase is not User u || u.access_hash == 0) + return null; + + return new InputPeerUser(u.id, u.access_hash); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to resolve system peer"); + return null; + } + } + + private async Task GetOrCreateConnectedClientAsync(int accountId) + { + var existing = _clientPool.GetClient(accountId); + if (existing?.User != null) + return existing; + + var account = await _accountManagement.GetAccountAsync(accountId) + ?? throw new InvalidOperationException($"账号不存在:{accountId}"); + + var apiId = ResolveApiId(account); + var apiHash = ResolveApiHash(account); + var sessionKey = ResolveSessionKey(account, apiHash); + + if (string.IsNullOrWhiteSpace(account.SessionPath)) + throw new InvalidOperationException("账号缺少 SessionPath,无法创建 Telegram 客户端"); + + var absoluteSessionPath = Path.GetFullPath(account.SessionPath); + if (File.Exists(absoluteSessionPath) && SessionDataConverter.LooksLikeSqliteSession(absoluteSessionPath)) + { + var ok = await SessionDataConverter.TryConvertSqliteSessionFromJsonAsync( + phone: account.Phone, + apiId: account.ApiId, + apiHash: account.ApiHash, + sqliteSessionPath: absoluteSessionPath, + logger: _logger + ); + + if (!ok) + { + throw new InvalidOperationException( + $"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},无法自动转换为可用 session。" + + "建议:重新导入包含 session_string 的 json,或到【账号-手机号登录】重新登录生成新的 sessions/*.session。"); + } + } + + await _clientPool.RemoveClientAsync(accountId); + var client = await _clientPool.GetOrCreateClientAsync( + accountId: accountId, + apiId: apiId, + apiHash: apiHash, + sessionPath: account.SessionPath, + sessionKey: sessionKey, + phoneNumber: account.Phone, + userId: account.UserId > 0 ? account.UserId : null); + + try + { + await client.ConnectAsync(); + if (client.User == null && (client.UserId != 0 || account.UserId != 0)) + await client.LoginUserIfNeeded(reloginOnFailedResume: false); + } + catch (Exception ex) + { + if (LooksLikeSessionApiMismatchOrCorrupted(ex)) + { + throw new InvalidOperationException( + "该账号的 Session 文件无法解析(通常是 ApiId/ApiHash 与生成 session 时不一致,或 session 文件已损坏)。" + + "请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再试。", + ex); + } + + throw new InvalidOperationException($"Telegram 会话加载失败:{ex.Message}", ex); + } + + if (client.User == null) + throw new InvalidOperationException("账号未登录或 session 已失效,请重新登录生成新的 session"); + + return client; + } + + private int ResolveApiId(Account account) + { + if (int.TryParse(_configuration["Telegram:ApiId"], out var globalApiId) && globalApiId > 0) + return globalApiId; + if (account.ApiId > 0) + return account.ApiId; + throw new InvalidOperationException("未配置全局 ApiId,且账号缺少 ApiId"); + } + + private string ResolveApiHash(Account account) + { + var global = _configuration["Telegram:ApiHash"]; + if (!string.IsNullOrWhiteSpace(global)) + return global.Trim(); + if (!string.IsNullOrWhiteSpace(account.ApiHash)) + return account.ApiHash.Trim(); + throw new InvalidOperationException("未配置全局 ApiHash,且账号缺少 ApiHash"); + } + + private static string ResolveSessionKey(Account account, string apiHash) + { + return !string.IsNullOrWhiteSpace(account.ApiHash) ? account.ApiHash.Trim() : apiHash.Trim(); + } + + private static bool LooksLikeSessionApiMismatchOrCorrupted(Exception ex) + { + var msg = ex.Message ?? string.Empty; + return msg.Contains("Can't read session block", StringComparison.OrdinalIgnoreCase) + || msg.Contains("Use the correct api_hash", StringComparison.OrdinalIgnoreCase) + || msg.Contains("Use the correct api_hash/id/key", StringComparison.OrdinalIgnoreCase); + } + + private static string BuildProfileDetails(TelegramAccountProfile profile) + { + var flags = new List(); + if (profile.IsPremium) flags.Add("Premium"); + if (profile.IsVerified) flags.Add("Verified"); + if (profile.IsRestricted) flags.Add("Restricted"); + if (profile.IsScam) flags.Add("Scam"); + if (profile.IsFake) flags.Add("Fake"); + if (profile.IsDeleted) flags.Add("Deleted"); + + var flagText = flags.Count == 0 ? "无" : string.Join(", ", flags); + return $"昵称:{profile.DisplayName};用户名:{profile.Username ?? "-"};UserId:{profile.UserId};标记:{flagText}"; + } + + private static (string summary, string details) MapTelegramException(Exception ex) + { + var msg = ex.Message ?? string.Empty; + + if (msg.Contains("FROZEN_METHOD_INVALID", StringComparison.OrdinalIgnoreCase)) + return ("接口被冻结(账号/ApiId 受限)", msg); + + if (msg.Contains("AUTH_KEY_UNREGISTERED", StringComparison.OrdinalIgnoreCase)) + return ("Session 失效(AUTH_KEY_UNREGISTERED)", msg); + + if (msg.Contains("SESSION_PASSWORD_NEEDED", StringComparison.OrdinalIgnoreCase)) + return ("需要两步验证密码(SESSION_PASSWORD_NEEDED)", msg); + + if (msg.Contains("PHONE_NUMBER_BANNED", StringComparison.OrdinalIgnoreCase) + || msg.Contains("USER_DEACTIVATED_BAN", StringComparison.OrdinalIgnoreCase)) + return ("账号被封禁/停用", msg); + + if (msg.Contains("Can't read session block", StringComparison.OrdinalIgnoreCase)) + return ("Session 无法读取(ApiHash/Key 不匹配或损坏)", msg); + + return ("连接失败", msg); + } +}