新增:从 session_string 自动转换为 WTelegram session

This commit is contained in:
meoacgx committed 2025-12-19 12:07:56 +08:00
1 parent 5136bd00c8
commit 624faca6c9
4 files changed
+234 -45

No files matched your search

@@ -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<bool> TryCreateSessionFromSessionKeyAsync(int apiId, string apiHash, string sessionPath, string sessionKey, long? userId)
private async Task<bool> 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<byte>();
}
}
private static bool TryGetString(System.Text.Json.JsonElement root, out string? value, params string[] names)
{
foreach (var name in names)
@@ -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<byte> 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)
@@ -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<byte> 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);
}
}
@@ -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<bool> 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<byte> 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<byte>();
}
}
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
{
// 忽略备份失败
}
}
}