修复:Telethon session_string 自动转换为 WTelegram 会话并可同步
This commit is contained in:
1 parent
624faca6c9
commit
4df30a137f
8 files changed
+373
-128
No files matched your search
@@ -14,6 +14,9 @@ obj/
|
||||
# Serena
|
||||
.serena/
|
||||
|
||||
# Local temp
|
||||
.tmp/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
@@ -11,7 +11,7 @@ public interface ITelegramClientPool
|
||||
/// <summary>
|
||||
/// 获取或创建指定账号的客户端
|
||||
/// </summary>
|
||||
Task<Client> GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath);
|
||||
Task<Client> GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath, string? phoneNumber = null, long? userId = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取已存在的客户端
|
||||
|
||||
@@ -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<bool> 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<byte>();
|
||||
}
|
||||
}
|
||||
// session_string 转换逻辑已收敛到 SessionDataConverter
|
||||
|
||||
private static bool TryGetString(System.Text.Json.JsonElement root, out string? value, params string[] names)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<bool> TryConvertSessionFromSessionDataAsync(
|
||||
public static async Task<bool> 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<bool> 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/<phone>.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数据/<phone> 下找
|
||||
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<bool> 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 内集中处理
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable
|
||||
|
||||
public int ActiveClientCount => _clients.Count;
|
||||
|
||||
public async Task<Client> GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath)
|
||||
public async Task<Client> 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;
|
||||
|
||||
Reference in new issue
Block a user