From 624faca6c97aa901eae416530171b0ca8cd2789f Mon Sep 17 00:00:00 2001 From: meoacgx Date: Fri, 19 Dec 2025 12:07:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E4=BB=8E=20session?= =?UTF-8?q?=5Fstring=20=E8=87=AA=E5=8A=A8=E8=BD=AC=E6=8D=A2=E4=B8=BA=20WTe?= =?UTF-8?q?legram=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/Telegram/AccountImportService.cs | 70 +++++++-- .../Services/Telegram/ChannelService.cs | 32 ++-- .../Services/Telegram/GroupService.cs | 32 ++-- .../Services/Telegram/SessionDataConverter.cs | 145 ++++++++++++++++++ 4 files changed, 234 insertions(+), 45 deletions(-) create mode 100644 src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs index 43349c5..8270d5a 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs @@ -348,12 +348,11 @@ public class AccountImportService return new ImportResult(false, phone, userId, username, null, "该 .session 为 SQLite 格式且缺少 session_string,无法导入(请重新登录生成新 session)"); } - var ok = await TryCreateSessionFromSessionKeyAsync( + var ok = await TryCreateSessionFromSessionStringAsync( apiId: apiId, apiHash: apiHash.Trim(), sessionPath: targetSessionPath, - sessionKey: sessionKey, - userId: userId + sessionString: sessionKey ); if (!ok) @@ -438,32 +437,69 @@ public class AccountImportService } } - private async Task TryCreateSessionFromSessionKeyAsync(int apiId, string apiHash, string sessionPath, string sessionKey, long? userId) + private async Task TryCreateSessionFromSessionStringAsync(int apiId, string apiHash, string sessionPath, string sessionString) { try { - string Config(string what) => what switch - { - "api_id" => apiId.ToString(), - "api_hash" => apiHash, - "session_pathname" => sessionPath, - "session_key" => sessionKey, - "user_id" => userId?.ToString() ?? null!, - _ => null! - }; + var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( + phone: Path.GetFileNameWithoutExtension(sessionPath), + apiId: apiId, + apiHash: apiHash, + targetSessionPath: sessionPath, + logger: _logger + ); - using var client = new Client(Config); - await client.ConnectAsync(); - return client.User != null; + // 上面的转换是从 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_key"); + _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(); + } + } + private static bool TryGetString(System.Text.Json.JsonElement root, out string? value, params string[] names) { foreach (var name in names) diff --git a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs index 3f81aeb..d9426c2 100644 --- a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs @@ -257,7 +257,23 @@ public class ChannelService : IChannelService var absoluteSessionPath = Path.GetFullPath(account.SessionPath); if (File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath)) - throw new InvalidOperationException($"该账号的 Session 文件为 SQLite 格式(常见于 Telethon/Pyrogram/Telegram Desktop):{account.SessionPath}。本项目基于 WTelegramClient,无法直接复用该格式;请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); + { + var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( + phone: account.Phone, + apiId: account.ApiId, + apiHash: account.ApiHash, + targetSessionPath: absoluteSessionPath, + logger: _logger + ); + + if (!ok) + { + throw new InvalidOperationException( + $"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" + + "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" + + "请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); + } + } await _clientPool.RemoveClientAsync(accountId); var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath); @@ -295,19 +311,7 @@ public class ChannelService : IChannelService private static bool LooksLikeSqliteSession(string filePath) { - try - { - using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - Span header = stackalloc byte[16]; - var read = fs.Read(header); - if (read < 15) return false; - var text = System.Text.Encoding.ASCII.GetString(header[..15]); - return string.Equals(text, "SQLite format 3", StringComparison.Ordinal); - } - catch - { - return false; - } + return SessionDataConverter.LooksLikeSqliteSession(filePath); } private static ChatAdminRights ConvertAdminRights(Interfaces.AdminRights rights) diff --git a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs index 587757a..b0a8444 100644 --- a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs @@ -124,7 +124,23 @@ public class GroupService : IGroupService var absoluteSessionPath = Path.GetFullPath(account.SessionPath); if (System.IO.File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath)) - throw new InvalidOperationException($"该账号的 Session 文件为 SQLite 格式(常见于 Telethon/Pyrogram/Telegram Desktop):{account.SessionPath}。本项目基于 WTelegramClient,无法直接复用该格式;请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); + { + var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync( + phone: account.Phone, + apiId: account.ApiId, + apiHash: account.ApiHash, + targetSessionPath: absoluteSessionPath, + logger: _logger + ); + + if (!ok) + { + throw new InvalidOperationException( + $"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" + + "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" + + "请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。"); + } + } await _clientPool.RemoveClientAsync(accountId); var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath); @@ -162,18 +178,6 @@ public class GroupService : IGroupService private static bool LooksLikeSqliteSession(string filePath) { - try - { - using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - Span header = stackalloc byte[16]; - var read = fs.Read(header); - if (read < 15) return false; - var text = System.Text.Encoding.ASCII.GetString(header[..15]); - return string.Equals(text, "SQLite format 3", StringComparison.Ordinal); - } - catch - { - return false; - } + return SessionDataConverter.LooksLikeSqliteSession(filePath); } } diff --git a/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs new file mode 100644 index 0000000..e367b88 --- /dev/null +++ b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs @@ -0,0 +1,145 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using WTelegram; + +namespace TelegramPanel.Core.Services.Telegram; + +internal static class SessionDataConverter +{ + public static async Task TryConvertSessionFromSessionDataAsync( + string phone, + int apiId, + string apiHash, + string targetSessionPath, + ILogger logger) + { + try + { + var jsonPath = TryFindSessionDataJson(phone); + if (string.IsNullOrWhiteSpace(jsonPath)) + return false; + + var jsonText = await File.ReadAllTextAsync(jsonPath); + using var doc = JsonDocument.Parse(jsonText); + + if (!doc.RootElement.TryGetProperty("session_string", out var sessionProp) || sessionProp.ValueKind != JsonValueKind.String) + return false; + + var sessionString = sessionProp.GetString(); + if (string.IsNullOrWhiteSpace(sessionString)) + return false; + + var sessionBytes = DecodeBase64Url(sessionString.Trim()); + if (sessionBytes.Length == 0) + return false; + + // 备份旧文件(如果存在) + TryBackupIfExists(targetSessionPath, suffix: "bak"); + + 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); + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to convert session for {Phone} from session数据 json", phone); + return false; + } + } + + public static bool LooksLikeSqliteSession(string filePath) + { + try + { + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + Span header = stackalloc byte[16]; + var read = fs.Read(header); + if (read < 15) return false; + var text = Encoding.ASCII.GetString(header[..15]); + return string.Equals(text, "SQLite format 3", StringComparison.Ordinal); + } + catch + { + return false; + } + } + + private static string? TryFindSessionDataJson(string phone) + { + 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 dirCandidate = Path.Combine(current, "session数据", phone); + if (Directory.Exists(dirCandidate)) + { + var any = Directory.EnumerateFiles(dirCandidate, "*.json", SearchOption.TopDirectoryOnly).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(any)) + return any; + } + + current = Directory.GetParent(current)?.FullName; + } + + return null; + } + + 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(); + } + } + + private static void TryBackupIfExists(string path, string suffix) + { + try + { + var fullPath = Path.GetFullPath(path); + if (!File.Exists(fullPath)) + return; + + 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); + } + catch + { + // 忽略备份失败 + } + } +} +