新增:从 session_string 自动转换为 WTelegram session
This commit is contained in:
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
|
||||
{
|
||||
// 忽略备份失败
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user