新增账号诊断/系统通知/在线设备服务

This commit is contained in:
meoacgx committed 2025-12-19 14:29:37 +08:00
1 parent ce5c23cee8
commit ccbb2aebed
5 files changed
+441

No files matched your search

@@ -0,0 +1,61 @@
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Core.Models;
/// <summary>
/// 账号状态检测结果(用于诊断账号是否可正常连通 Telegram)
/// </summary>
public record TelegramAccountStatusResult(
bool Ok,
string Summary,
string? Details,
DateTime CheckedAtUtc,
TelegramAccountProfile? Profile = null
);
/// <summary>
/// Telegram 账号资料快照
/// </summary>
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;
}
}
@@ -0,0 +1,32 @@
namespace TelegramPanel.Core.Models;
/// <summary>
/// 在线设备 / 会话信息(来自 account.getAuthorizations)
/// </summary>
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}";
}
}
}
@@ -0,0 +1,11 @@
namespace TelegramPanel.Core.Models;
/// <summary>
/// Telegram 系统通知(常用于接收登录验证码,来自 777000 对话)
/// </summary>
public record TelegramSystemMessage(
int Id,
DateTime? DateUtc,
string Text
);
@@ -20,6 +20,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IChannelService, ChannelService>();
services.AddScoped<IGroupService, GroupService>();
services.AddScoped<ISessionImporter, SessionImporter>();
services.AddScoped<AccountTelegramToolsService>();
// 注册账号导入协调服务
services.AddScoped<AccountImportService>();
@@ -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;
/// <summary>
/// 账号诊断 / 系统通知 / 在线设备管理
/// </summary>
public class AccountTelegramToolsService
{
private const long TelegramSystemUserId = 777000;
private readonly AccountManagementService _accountManagement;
private readonly ITelegramClientPool _clientPool;
private readonly IConfiguration _configuration;
private readonly ILogger<AccountTelegramToolsService> _logger;
public AccountTelegramToolsService(
AccountManagementService accountManagement,
ITelegramClientPool clientPool,
IConfiguration configuration,
ILogger<AccountTelegramToolsService> logger)
{
_accountManagement = accountManagement;
_clientPool = clientPool;
_configuration = configuration;
_logger = logger;
}
public async Task<TelegramAccountStatusResult> 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<User>().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<IReadOnlyList<TelegramSystemMessage>> 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<TelegramSystemMessage>();
var history = await client.Messages_GetHistory(peer, limit: limit);
var list = new List<TelegramSystemMessage>(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<IReadOnlyList<TelegramAuthorizationInfo>> GetAuthorizationsAsync(int accountId)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
var auths = await client.Account_GetAuthorizations();
var list = new List<TelegramAuthorizationInfo>(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<bool> KickAuthorizationAsync(int accountId, long authorizationHash)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
var ok = await client.Account_ResetAuthorization(authorizationHash);
return ok;
}
public async Task<bool> KickAllOtherAuthorizationsAsync(int accountId)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
var ok = await client.Auth_ResetAuthorizations();
return ok;
}
private async Task<InputPeerUser?> 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<Client> 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<string>();
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);
}
}