diff --git a/.gitignore b/.gitignore index 8b8b3d2..2252c96 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ obj/ # Serena .serena/ +# Local temp +.tmp/ + # Logs logs/ *.log diff --git a/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs b/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs index 4b9a083..193af86 100644 --- a/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs +++ b/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs @@ -11,7 +11,7 @@ public interface ITelegramClientPool /// /// 获取或创建指定账号的客户端 /// - Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath); + Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath, string? phoneNumber = null, long? userId = null); /// /// 获取已存在的客户端 diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs index 8270d5a..600f90e 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs @@ -348,12 +348,14 @@ public class AccountImportService return new ImportResult(false, phone, userId, username, null, "该 .session 为 SQLite 格式且缺少 session_string,无法导入(请重新登录生成新 session)"); } - var ok = await TryCreateSessionFromSessionStringAsync( + var ok = await SessionDataConverter.TryCreateWTelegramSessionFromSessionStringAsync( + sessionString: sessionKey, apiId: apiId, apiHash: apiHash.Trim(), - sessionPath: targetSessionPath, - sessionString: sessionKey - ); + targetSessionPath: targetSessionPath, + phone: phone, + userId: userId, + logger: _logger); if (!ok) { @@ -437,68 +439,7 @@ public class AccountImportService } } - private async Task TryCreateSessionFromSessionStringAsync(int apiId, string apiHash, string sessionPath, string sessionString) - { - try - { - var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( - phone: Path.GetFileNameWithoutExtension(sessionPath), - apiId: apiId, - apiHash: apiHash, - targetSessionPath: sessionPath, - logger: _logger - ); - - // 上面的转换是从 session数据 json 来的;这里导入时并不一定落到 session数据, - // 所以当找不到 json 时,直接按 base64url 解码写文件并验证连接。 - if (!ok) - { - var bytes = DecodeBase64Url(sessionString); - if (bytes.Length == 0) return false; - - var sessionsDir = Path.GetDirectoryName(Path.GetFullPath(sessionPath)) ?? Directory.GetCurrentDirectory(); - Directory.CreateDirectory(sessionsDir); - await File.WriteAllBytesAsync(sessionPath, bytes); - - string Config(string what) => what switch - { - "api_id" => apiId.ToString(), - "api_hash" => apiHash, - "session_pathname" => sessionPath, - _ => null! - }; - - using var client = new Client(Config); - await client.ConnectAsync(); - ok = client.User != null; - } - - return ok; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to create session from session_string"); - try { if (File.Exists(sessionPath)) File.Delete(sessionPath); } catch { } - return false; - } - } - - private static byte[] DecodeBase64Url(string input) - { - var s = input.Replace('-', '+').Replace('_', '/'); - var mod = s.Length % 4; - if (mod == 2) s += "=="; - else if (mod == 3) s += "="; - - try - { - return Convert.FromBase64String(s); - } - catch - { - return Array.Empty(); - } - } + // session_string 转换逻辑已收敛到 SessionDataConverter private static bool TryGetString(System.Text.Json.JsonElement root, out string? value, params string[] names) { diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs index 481b995..a7c0aec 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs @@ -44,7 +44,7 @@ public class AccountService : IAccountService _logger.LogInformation("Starting login for phone {Phone}", phone); - var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath); + var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, phoneNumber: phone, userId: null); string result; try @@ -57,7 +57,7 @@ public class AccountService : IAccountService TryBackupCorruptedSessionIfExists(sessionPath); await _clientPool.RemoveClientAsync(accountId); - client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath); + client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, phoneNumber: phone, userId: null); result = await client.Login(phone); } diff --git a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs index d9426c2..e8b5261 100644 --- a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs @@ -258,11 +258,11 @@ public class ChannelService : IChannelService var absoluteSessionPath = Path.GetFullPath(account.SessionPath); if (File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath)) { - var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( + var ok = await SessionDataConverter.TryConvertSqliteSessionFromJsonAsync( phone: account.Phone, apiId: account.ApiId, apiHash: account.ApiHash, - targetSessionPath: absoluteSessionPath, + sqliteSessionPath: absoluteSessionPath, logger: _logger ); @@ -270,17 +270,19 @@ public class ChannelService : IChannelService { throw new InvalidOperationException( $"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" + - "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" + + "已尝试从本地 json(例如 sessions/<手机号>.json 或 session数据/<手机号>/*.json)读取 session_string 自动转换但失败;" + "请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); } } await _clientPool.RemoveClientAsync(accountId); - var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath); + var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath, account.Phone, account.UserId); try { await client.ConnectAsync(); + if (client.User == null && (client.UserId != 0 || account.UserId != 0)) + await client.LoginUserIfNeeded(reloginOnFailedResume: false); } catch (Exception ex) { diff --git a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs index b0a8444..cc30cbe 100644 --- a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs @@ -125,11 +125,11 @@ public class GroupService : IGroupService var absoluteSessionPath = Path.GetFullPath(account.SessionPath); if (System.IO.File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath)) { - var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( + var ok = await SessionDataConverter.TryConvertSqliteSessionFromJsonAsync( phone: account.Phone, apiId: account.ApiId, apiHash: account.ApiHash, - targetSessionPath: absoluteSessionPath, + sqliteSessionPath: absoluteSessionPath, logger: _logger ); @@ -137,17 +137,19 @@ public class GroupService : IGroupService { throw new InvalidOperationException( $"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" + - "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" + + "已尝试从本地 json(例如 sessions/<手机号>.json 或 session数据/<手机号>/*.json)读取 session_string 自动转换但失败;" + "请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); } } await _clientPool.RemoveClientAsync(accountId); - var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath); + var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath, account.Phone, account.UserId); try { await client.ConnectAsync(); + if (client.User == null && (client.UserId != 0 || account.UserId != 0)) + await client.LoginUserIfNeeded(reloginOnFailedResume: false); } catch (Exception ex) { diff --git a/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs index e367b88..fbce9a6 100644 --- a/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs +++ b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs @@ -1,22 +1,28 @@ +using System.Buffers.Binary; +using System.Net; +using System.Reflection; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; +using TL; using WTelegram; namespace TelegramPanel.Core.Services.Telegram; internal static class SessionDataConverter { - public static async Task TryConvertSessionFromSessionDataAsync( + public static async Task TryConvertSqliteSessionFromJsonAsync( string phone, int apiId, string apiHash, - string targetSessionPath, + string sqliteSessionPath, ILogger logger) { try { - var jsonPath = TryFindSessionDataJson(phone); + var absoluteSqliteSessionPath = Path.GetFullPath(sqliteSessionPath); + var jsonPath = TryFindAnySessionJsonPath(phone, absoluteSqliteSessionPath); if (string.IsNullOrWhiteSpace(jsonPath)) return false; @@ -30,37 +36,95 @@ internal static class SessionDataConverter if (string.IsNullOrWhiteSpace(sessionString)) return false; - var sessionBytes = DecodeBase64Url(sessionString.Trim()); - if (sessionBytes.Length == 0) - return false; + _ = doc.RootElement.TryGetProperty("user_id", out var userIdProp); + _ = doc.RootElement.TryGetProperty("uid", out var uidProp); + long? userId = null; + if (userIdProp.ValueKind == JsonValueKind.Number && userIdProp.TryGetInt64(out var uid1)) userId = uid1; + if (userId == null && uidProp.ValueKind == JsonValueKind.Number && uidProp.TryGetInt64(out var uid2)) userId = uid2; - // 备份旧文件(如果存在) - TryBackupIfExists(targetSessionPath, suffix: "bak"); + var ok = await TryCreateWTelegramSessionFromSessionStringAsync( + sessionString: sessionString.Trim(), + apiId: apiId, + apiHash: apiHash, + targetSessionPath: absoluteSqliteSessionPath, + phone: phone, + userId: userId, + logger: logger); + if (!ok) return false; - var sessionsDir = Path.GetDirectoryName(Path.GetFullPath(targetSessionPath)) ?? Directory.GetCurrentDirectory(); - Directory.CreateDirectory(sessionsDir); - await File.WriteAllBytesAsync(targetSessionPath, sessionBytes); - - // 用 WTelegram 连接一次验证 session 是否可用 - string Config(string what) => what switch - { - "api_id" => apiId.ToString(), - "api_hash" => apiHash, - "session_pathname" => targetSessionPath, - _ => null! - }; - - using var client = new Client(Config); - await client.ConnectAsync(); - if (client.User == null) - return false; - - logger.LogInformation("Converted session for {Phone} from session数据 json: {JsonPath}", phone, jsonPath); + logger.LogInformation("Converted sqlite session for {Phone} using json: {JsonPath}", phone, jsonPath); return true; } catch (Exception ex) { - logger.LogWarning(ex, "Failed to convert session for {Phone} from session数据 json", phone); + logger.LogWarning(ex, "Failed to convert sqlite session for {Phone} from json", phone); + return false; + } + } + + public static async Task TryCreateWTelegramSessionFromSessionStringAsync( + string sessionString, + int apiId, + string apiHash, + string targetSessionPath, + string phone, + long? userId, + ILogger logger) + { + string? backupPath = null; + try + { + if (string.IsNullOrWhiteSpace(sessionString)) + return false; + + var absoluteTargetSessionPath = Path.GetFullPath(targetSessionPath); + var normalizedPhone = NormalizePhone(phone); + if (string.IsNullOrWhiteSpace(normalizedPhone)) + normalizedPhone = NormalizePhone(Path.GetFileNameWithoutExtension(absoluteTargetSessionPath)); + + if (!TryParseTelethonStringSession(sessionString.Trim(), out var telethon)) + return false; + + // 先备份旧 sqlite session,再生成 WTelegram session 覆盖原路径 + if (File.Exists(absoluteTargetSessionPath)) + { + var suffix = LooksLikeSqliteSession(absoluteTargetSessionPath) ? "sqlite.bak" : "bak"; + backupPath = BuildBackupPath(absoluteTargetSessionPath, suffix); + Directory.CreateDirectory(Path.GetDirectoryName(backupPath) ?? Directory.GetCurrentDirectory()); + File.Move(absoluteTargetSessionPath, backupPath, overwrite: true); + } + + var sessionsDir = Path.GetDirectoryName(absoluteTargetSessionPath) ?? Directory.GetCurrentDirectory(); + Directory.CreateDirectory(sessionsDir); + + // 使用 WTelegram 的 Session 存储格式生成可用 session 文件(加密 JSON) + var ok = await WriteWTelegramSessionFileAsync( + apiId: apiId, + apiHash: apiHash, + sessionPath: absoluteTargetSessionPath, + phoneDigits: normalizedPhone, + userId: userId, + dcId: telethon.DcId, + ipAddress: telethon.IpAddress, + port: telethon.Port, + authKey: telethon.AuthKey, + logger: logger + ); + if (!ok) + throw new InvalidOperationException("WTelegram session 生成失败或校验失败"); + + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to create WTelegram session from session_string"); + try { if (File.Exists(targetSessionPath)) File.Delete(targetSessionPath); } catch { } + try + { + if (!string.IsNullOrWhiteSpace(backupPath) && File.Exists(backupPath) && !File.Exists(targetSessionPath)) + File.Move(backupPath, targetSessionPath, overwrite: true); + } + catch { } return false; } } @@ -82,24 +146,53 @@ internal static class SessionDataConverter } } - private static string? TryFindSessionDataJson(string phone) + private static string? TryFindAnySessionJsonPath(string phone, string absoluteSessionPath) { - var current = Directory.GetCurrentDirectory(); - for (int i = 0; i < 6 && !string.IsNullOrWhiteSpace(current); i++) - { - var candidate = Path.Combine(current, "session数据", phone, $"{phone}.json"); - if (File.Exists(candidate)) - return candidate; + var normalizedPhone = NormalizePhone(phone); + if (string.IsNullOrWhiteSpace(normalizedPhone)) + return null; - var dirCandidate = Path.Combine(current, "session数据", phone); - if (Directory.Exists(dirCandidate)) + // 1) 优先找与 session 同目录的 phone.json(最常见:sessions/.json) + var sessionDir = Path.GetDirectoryName(absoluteSessionPath); + if (!string.IsNullOrWhiteSpace(sessionDir)) + { + var direct = Path.Combine(sessionDir, $"{normalizedPhone}.json"); + if (File.Exists(direct)) + return direct; + + var anyInSessionDir = Directory.EnumerateFiles(sessionDir, "*.json", SearchOption.TopDirectoryOnly) + .FirstOrDefault(p => string.Equals(NormalizePhone(Path.GetFileNameWithoutExtension(p)), normalizedPhone, StringComparison.Ordinal)); + if (!string.IsNullOrWhiteSpace(anyInSessionDir)) + return anyInSessionDir; + } + + // 2) 尝试在仓库根目录的 session数据/ 下找 + var repoRoot = TryFindRepoRoot(); + if (!string.IsNullOrWhiteSpace(repoRoot)) + { + var sessionDataDir = Path.Combine(repoRoot, "session数据", normalizedPhone); + var direct = Path.Combine(sessionDataDir, $"{normalizedPhone}.json"); + if (File.Exists(direct)) + return direct; + + if (Directory.Exists(sessionDataDir)) { - var any = Directory.EnumerateFiles(dirCandidate, "*.json", SearchOption.TopDirectoryOnly).FirstOrDefault(); + var any = Directory.EnumerateFiles(sessionDataDir, "*.json", SearchOption.TopDirectoryOnly).FirstOrDefault(); if (!string.IsNullOrWhiteSpace(any)) return any; } - current = Directory.GetParent(current)?.FullName; + // 3) 兜底:在 session数据 下递归扫描 phone 字段匹配 + var sessionDataRoot = Path.Combine(repoRoot, "session数据"); + var scanned = TryScanJsonByPhone(sessionDataRoot, normalizedPhone); + if (!string.IsNullOrWhiteSpace(scanned)) + return scanned; + + // 4) 再兜底:在 sessions 下递归扫描(部分用户会把 json 放在 sessions/ 里) + var sessionsRoot = Path.Combine(repoRoot, "sessions"); + scanned = TryScanJsonByPhone(sessionsRoot, normalizedPhone); + if (!string.IsNullOrWhiteSpace(scanned)) + return scanned; } return null; @@ -122,24 +215,210 @@ internal static class SessionDataConverter } } - private static void TryBackupIfExists(string path, string suffix) + private readonly record struct TelethonSessionData(int DcId, string IpAddress, ushort Port, byte[] AuthKey); + + private static bool TryParseTelethonStringSession(string sessionString, out TelethonSessionData data) { try { - var fullPath = Path.GetFullPath(path); - if (!File.Exists(fullPath)) - return; + data = default; + if (string.IsNullOrWhiteSpace(sessionString) || sessionString.Length < 16) + return false; - var dir = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory(); - var name = Path.GetFileNameWithoutExtension(fullPath); - var ext = Path.GetExtension(fullPath); - var backupPath = Path.Combine(dir, $"{name}.{suffix}{ext}"); - File.Move(fullPath, backupPath, overwrite: true); + // Telethon StringSession: first char is version digit + if (!char.IsDigit(sessionString[0])) + return false; + + var body = sessionString.Substring(1); + var packed = DecodeBase64Url(body); + + // 常见 Telethon packed bytes 长度: + // - IPv4: 263 = 1(dc_id)+4(ip)+2(port)+256(auth_key) + // - IPv6: 275 = 1+16+2+256 + if (packed.Length is not (263 or 275)) + return false; + + var dcId = packed[0]; + var ipLen = packed.Length - 1 - 2 - 256; + if (ipLen is not (4 or 16)) + return false; + + var ipBytes = packed.AsSpan(1, ipLen).ToArray(); + var ip = new IPAddress(ipBytes).ToString(); + var port = BinaryPrimitives.ReadUInt16BigEndian(packed.AsSpan(1 + ipLen, 2)); + var authKey = packed.AsSpan(1 + ipLen + 2, 256).ToArray(); + + data = new TelethonSessionData(dcId, ip, port, authKey); + return true; } catch { - // 忽略备份失败 + data = default; + return false; } } -} + private static async Task WriteWTelegramSessionFileAsync( + int apiId, + string apiHash, + string sessionPath, + string phoneDigits, + long? userId, + int dcId, + string ipAddress, + ushort port, + byte[] authKey, + ILogger logger) + { + string Config(string what) => what switch + { + "api_id" => apiId.ToString(), + "api_hash" => apiHash, + "session_key" => apiHash, + "session_pathname" => sessionPath, + "phone_number" => phoneDigits, + "user_id" => userId?.ToString() ?? "-1", + _ => null! + }; + + // 1) 创建空 session 文件 + await using (var builder = new Client(Config)) + { + var clientType = typeof(Client); + var sessionField = clientType.GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("无法访问 WTelegram.Client._session"); + var dcSessionField = clientType.GetField("_dcSession", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("无法访问 WTelegram.Client._dcSession"); + + var sessionObj = sessionField.GetValue(builder) ?? throw new InvalidOperationException("WTelegram session 未初始化"); + var sessionType = sessionObj.GetType(); + + var dcSessionType = sessionType.GetNestedType("DCSession", BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("无法访问 WTelegram.Session.DCSession"); + var dcSessionObj = Activator.CreateInstance(dcSessionType) ?? throw new InvalidOperationException("无法创建 DCSession"); + + // TL.DcOption 是公开类型,直接构造 + var dcOption = new DcOption { id = dcId, ip_address = ipAddress, port = port, flags = 0 }; + + // 填充 DCSession:AuthKey + DataCenter + UserId + authKeyID + dcSessionType.GetField("AuthKey")?.SetValue(dcSessionObj, authKey); + dcSessionType.GetField("UserId")?.SetValue(dcSessionObj, userId ?? 0); + dcSessionType.GetField("DataCenter")?.SetValue(dcSessionObj, dcOption); + dcSessionType.GetField("Layer")?.SetValue(dcSessionObj, 0); + + using (var sha1 = SHA1.Create()) + { + var hash = sha1.ComputeHash(authKey); + var authKeyId = BinaryPrimitives.ReadInt64LittleEndian(hash.AsSpan(12, 8)); + dcSessionType.GetField("authKeyID", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(dcSessionObj, authKeyId); + } + + dcSessionType.GetField("Client", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(dcSessionObj, builder); + + // 填充 Session:MainDC + UserId + DcOptions + DCSessions + sessionType.GetField("MainDC")?.SetValue(sessionObj, dcId); + sessionType.GetField("UserId")?.SetValue(sessionObj, userId ?? 0); + sessionType.GetField("DcOptions")?.SetValue(sessionObj, new[] { dcOption }); + + var dcSessionsField = sessionType.GetField("DCSessions"); + var dcSessions = dcSessionsField?.GetValue(sessionObj) as System.Collections.IDictionary; + if (dcSessions == null) + throw new InvalidOperationException("无法访问 Session.DCSessions"); + dcSessions[dcId] = dcSessionObj; + + // 同步 builder 当前 dcSession 引用 + dcSessionField.SetValue(builder, dcSessionObj); + + // 保存 session(写入加密 JSON) + var save = sessionType.GetMethod("Save", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("无法访问 Session.Save()"); + lock (sessionObj) save.Invoke(sessionObj, null); + } + + // 2) 用 LoginUserIfNeeded 验证(不允许自动重新登录,避免进入要验证码流程) + await using var probe = new Client(Config); + try + { + _ = await probe.LoginUserIfNeeded(reloginOnFailedResume: false); + logger.LogInformation("WTelegram session validated for {Phone} on DC {DcId}", phoneDigits, dcId); + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "WTelegram session validation failed for {Phone}", phoneDigits); + return false; + } + } + + private static string BuildBackupPath(string originalPath, string suffix) + { + var fullPath = Path.GetFullPath(originalPath); + var dir = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory(); + var name = Path.GetFileNameWithoutExtension(fullPath); + var ext = Path.GetExtension(fullPath); + return Path.Combine(dir, $"{name}.{suffix}{ext}"); + } + + private static string? TryFindRepoRoot() + { + var current = Directory.GetCurrentDirectory(); + for (int i = 0; i < 10 && !string.IsNullOrWhiteSpace(current); i++) + { + if (File.Exists(Path.Combine(current, "TelegramPanel.sln"))) + return current; + current = Directory.GetParent(current)?.FullName; + } + + return null; + } + + private static string? TryScanJsonByPhone(string rootDir, string normalizedPhone) + { + try + { + if (!Directory.Exists(rootDir)) + return null; + + foreach (var jsonPath in Directory.EnumerateFiles(rootDir, "*.json", SearchOption.AllDirectories)) + { + try + { + var text = File.ReadAllText(jsonPath); + using var doc = JsonDocument.Parse(text); + if (doc.RootElement.TryGetProperty("phone", out var phoneProp) && phoneProp.ValueKind == JsonValueKind.String) + { + var p = NormalizePhone(phoneProp.GetString()); + if (string.Equals(p, normalizedPhone, StringComparison.Ordinal)) + return jsonPath; + } + } + catch + { + // ignore single json parse error + } + } + + return null; + } + catch + { + return null; + } + } + + private static string NormalizePhone(string? phone) + { + if (string.IsNullOrWhiteSpace(phone)) + return string.Empty; + + var digits = new StringBuilder(phone.Length); + foreach (var ch in phone) + { + if (ch >= '0' && ch <= '9') + digits.Append(ch); + } + return digits.ToString(); + } + + // 备份逻辑在 TryCreateWTelegramSessionFromSessionStringAsync 内集中处理 +} diff --git a/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs b/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs index 05221b8..ad17205 100644 --- a/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs +++ b/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs @@ -23,7 +23,7 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable public int ActiveClientCount => _clients.Count; - public async Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath) + public async Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath, string? phoneNumber = null, long? userId = null) { if (_clients.TryGetValue(accountId, out var existingClient)) { @@ -49,6 +49,7 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable _logger.LogInformation("Creating new Telegram client for account {AccountId}", accountId); // 使用 config 回调设置 session 路径 + phoneNumber = NormalizePhone(phoneNumber); string Config(string what) { return what switch @@ -56,6 +57,8 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable "api_id" => apiId.ToString(), "api_hash" => apiHash, "session_pathname" => sessionPath, + "phone_number" => string.IsNullOrWhiteSpace(phoneNumber) ? null! : phoneNumber, + "user_id" => userId.HasValue && userId.Value > 0 ? userId.Value.ToString() : null!, _ => null! // 使用 null! 抑制警告,这是 WTelegramClient 的预期行为 }; } @@ -78,6 +81,21 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable } } + private static string? NormalizePhone(string? phone) + { + if (string.IsNullOrWhiteSpace(phone)) + return null; + + var digits = new char[phone.Length]; + var count = 0; + foreach (var ch in phone) + { + if (ch >= '0' && ch <= '9') + digits[count++] = ch; + } + return count == 0 ? null : new string(digits, 0, count); + } + public Client? GetClient(int accountId) { return _clients.TryGetValue(accountId, out var client) ? client : null;