407 lines
16 KiB
C#
407 lines
16 KiB
C#
using System.Text.Json;
|
|
using Lagrange.Core.Common;
|
|
using Lagrange.Core.Internal.Context.Attributes;
|
|
using Lagrange.Core.Internal.Event.EventArg;
|
|
using Lagrange.Core.Internal.Event.Protocol;
|
|
using Lagrange.Core.Internal.Event.Protocol.Login;
|
|
using Lagrange.Core.Internal.Event.Protocol.System;
|
|
using Lagrange.Core.Internal.Packets.Login.NTLogin;
|
|
using Lagrange.Core.Internal.Packets.Login.WtLogin.Entity;
|
|
using Lagrange.Core.Internal.Service;
|
|
using Lagrange.Core.Utility.Crypto;
|
|
using Lagrange.Core.Utility.Network;
|
|
|
|
// ReSharper disable AsyncVoidLambda
|
|
|
|
namespace Lagrange.Core.Internal.Context.Logic.Implementation;
|
|
|
|
[EventSubscribe(typeof(TransEmpEvent))]
|
|
[EventSubscribe(typeof(LoginEvent))]
|
|
[EventSubscribe(typeof(KickNTEvent))]
|
|
[BusinessLogic("WtExchangeLogic", "Manage the online task of the Bot")]
|
|
internal class WtExchangeLogic : LogicBase
|
|
{
|
|
private const string Tag = nameof(WtExchangeLogic);
|
|
|
|
private readonly TaskCompletionSource<bool> _qrCodeTask;
|
|
private readonly TaskCompletionSource<bool> _unusualTask;
|
|
private TaskCompletionSource<(string, string)>? _captchaTask;
|
|
|
|
private const string Interface = "https://ntlogin.qq.com/qr/getFace";
|
|
|
|
private const string QueryEvent = "wtlogin.trans_emp CMD0x12";
|
|
|
|
internal WtExchangeLogic(ContextCollection collection) : base(collection)
|
|
{
|
|
_qrCodeTask = new TaskCompletionSource<bool>();
|
|
_unusualTask = new TaskCompletionSource<bool>();
|
|
}
|
|
|
|
public override async Task Incoming(ProtocolEvent e)
|
|
{
|
|
switch (e)
|
|
{
|
|
case KickNTEvent kick:
|
|
Collection.Log.LogFatal(Tag, $"KickNTEvent: {kick.Tag}: {kick.Message}");
|
|
Collection.Log.LogFatal(Tag, "Bot will be offline in 5 seconds...");
|
|
await Task.Delay(5000);
|
|
|
|
Collection.Invoker.PostEvent(new BotOfflineEvent()); // TODO: Fill in the reason of offline
|
|
Collection.Scheduler.Dispose();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>1. resolve wtlogin.trans_emp CMD0x31 packet</para>
|
|
/// <para>2. Schedule wtlogin.trans_emp CMD0x12 Task</para>
|
|
/// </summary>
|
|
public async Task<(string, byte[])?> FetchQrCode()
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Connecting Servers...");
|
|
if (!await Collection.Socket.Connect()) return null;
|
|
Collection.Scheduler.Interval("Heartbeat.Alive", 10 * 1000, async () => await Collection.Business.PushEvent(AliveEvent.Create()));
|
|
|
|
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.FetchQrCode);
|
|
var result = await Collection.Business.SendEvent(transEmp);
|
|
|
|
if (result.Count != 0)
|
|
{
|
|
var @event = (TransEmpEvent)result[0];
|
|
Collection.Keystore.Session.QrString = @event.QrSig;
|
|
Collection.Keystore.Session.QrSign = @event.Signature;
|
|
Collection.Keystore.Session.QrUrl = @event.Url;
|
|
|
|
Collection.Log.LogInfo(Tag, $"QrCode Fetched, Expiration: {@event.Expiration} seconds");
|
|
return (@event.Url, @event.QrCode);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Task LoginByQrCode()
|
|
{
|
|
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () => await QueryQrCodeState());
|
|
return _qrCodeTask.Task;
|
|
}
|
|
|
|
public async Task<bool> LoginByPassword()
|
|
{
|
|
if (!Collection.Socket.Connected) // if socket not connected, try to connect
|
|
{
|
|
if (!await Collection.Socket.Connect()) return false;
|
|
Collection.Scheduler.Interval("Heartbeat.Alive", 10 * 1000, async () => await Collection.Business.PushEvent(AliveEvent.Create()));
|
|
}
|
|
|
|
if (Collection.Keystore.Session.ExchangeKey == null)
|
|
{
|
|
if (!await KeyExchange())
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Key Exchange Failed, please try again later");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (Collection.Keystore.Session.TempPassword != null) // try EasyLogin
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Trying to Login by EasyLogin...");
|
|
var easyLoginEvent = EasyLoginEvent.Create();
|
|
var easyLoginResult = await Collection.Business.SendEvent(easyLoginEvent);
|
|
|
|
if (easyLoginResult.Count != 0)
|
|
{
|
|
switch ((LoginCommon.Error)easyLoginResult[0].ResultCode)
|
|
{
|
|
case LoginCommon.Error.Success:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success");
|
|
|
|
await BotOnline();
|
|
return true;
|
|
}
|
|
case LoginCommon.Error.UnusualVerify:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success, but need to verify");
|
|
|
|
if (!await FetchUnusual())
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Fetch unusual state failed");
|
|
return false;
|
|
}
|
|
|
|
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () => await QueryUnusualState());
|
|
bool result = await _unusualTask.Task;
|
|
if (result) await BotOnline();
|
|
return result;
|
|
}
|
|
default:
|
|
{
|
|
Collection.Log.LogWarning(Tag, "Fast Login Failed, trying to Login by Password...");
|
|
|
|
Collection.Keystore.Session.TempPassword = null; // clear temp password
|
|
return await LoginByPassword(); // try password login
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Trying to Login by Password...");
|
|
var passwordLoginEvent = PasswordLoginEvent.Create();
|
|
var passwordLoginResult = await Collection.Business.SendEvent(passwordLoginEvent);
|
|
|
|
if (passwordLoginResult.Count != 0)
|
|
{
|
|
var @event = (PasswordLoginEvent)passwordLoginResult[0];
|
|
switch ((LoginCommon.Error)@event.ResultCode)
|
|
{
|
|
case LoginCommon.Error.Success:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success");
|
|
|
|
await BotOnline();
|
|
return true;
|
|
}
|
|
case LoginCommon.Error.UnusualVerify:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success, but need to verify");
|
|
|
|
await FetchUnusual();
|
|
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () => await QueryUnusualState());
|
|
return true;
|
|
}
|
|
case LoginCommon.Error.CaptchaVerify:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success, but captcha is required, please follow the link from event");
|
|
if (Collection.Keystore.Session.CaptchaUrl != null)
|
|
{
|
|
var captchaEvent = new BotCaptchaEvent(Collection.Keystore.Session.CaptchaUrl);
|
|
Collection.Invoker.PostEvent(captchaEvent);
|
|
|
|
string aid = Collection.Keystore.Session.CaptchaUrl.Split("&sid=")[1].Split("&")[0];
|
|
_captchaTask = new TaskCompletionSource<(string, string)>();
|
|
var (ticket, randStr) = await _captchaTask.Task;
|
|
Collection.Keystore.Session.Captcha = new ValueTuple<string, string, string>(ticket, randStr, aid);
|
|
|
|
return await LoginByPassword();
|
|
}
|
|
|
|
Collection.Log.LogInfo(Tag, "Captcha Url is null, please try again later");
|
|
return false;
|
|
}
|
|
default:
|
|
{
|
|
Collection.Log.LogWarning(Tag, @event is { Message: not null, Tag: not null }
|
|
? $"Login Failed: {(LoginCommon.Error)@event.ResultCode} | {@event.Tag}: {@event.Message}"
|
|
: $"Login Failed: {(LoginCommon.Error)@event.ResultCode}");
|
|
|
|
Collection.Invoker.Dispose();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> KeyExchange()
|
|
{
|
|
var keyExchangeEvent = KeyExchangeEvent.Create();
|
|
var exchangeResult = await Collection.Business.SendEvent(keyExchangeEvent);
|
|
if (exchangeResult.Count != 0)
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Key Exchange successfully!");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> DoWtLogin()
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Doing Login...");
|
|
Collection.Keystore.Session.Sequence = 0;
|
|
|
|
Collection.Keystore.SecpImpl = new EcdhImpl(EcdhImpl.CryptMethod.Secp192K1);
|
|
var loginEvent = LoginEvent.Create();
|
|
var result = await Collection.Business.SendEvent(loginEvent);
|
|
|
|
if (result.Count != 0)
|
|
{
|
|
var @event = (LoginEvent)result[0];
|
|
if (@event.ResultCode == 0)
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Login Success");
|
|
Collection.Keystore.Info = new BotKeystore.BotInfo(@event.Age, @event.Sex, @event.Name);
|
|
Collection.Log.LogInfo(Tag, Collection.Keystore.Info.ToString());
|
|
await BotOnline();
|
|
|
|
return true;
|
|
}
|
|
|
|
Collection.Log.LogFatal(Tag, $"Login failed: {@event.ResultCode}");
|
|
Collection.Log.LogFatal(Tag, $"Tag: {@event.Tag}\nState: {@event.Message}");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task QueryQrCodeState()
|
|
{
|
|
if (Collection.Keystore.Session.QrString == null)
|
|
{
|
|
Collection.Log.LogFatal(Tag, "QrString is null, Please Fetch QrCode First");
|
|
_qrCodeTask.SetResult(false);
|
|
return;
|
|
}
|
|
|
|
var request = new NTLoginHttpRequest
|
|
{
|
|
Appid = Collection.AppInfo.AppId,
|
|
Qrsig = Collection.Keystore.Session.QrString,
|
|
FaceUpdateTime = 0
|
|
};
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(request);
|
|
var response = await Http.PostAsync(Interface, payload, "application/json");
|
|
var info = JsonSerializer.Deserialize<NTLoginHttpResponse>(response);
|
|
if (info != null) Collection.Keystore.Uin = info.Uin;
|
|
|
|
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.QueryResult);
|
|
var result = await Collection.Business.SendEvent(transEmp);
|
|
|
|
if (result.Count != 0)
|
|
{
|
|
var @event = (TransEmpEvent)result[0];
|
|
var state = (TransEmp12.State)@event.ResultCode;
|
|
Collection.Log.LogInfo(Tag, $"QrCode State Queried: {state} Uin: {Collection.Keystore.Uin}");
|
|
|
|
switch (state)
|
|
{
|
|
case TransEmp12.State.Confirmed:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "QrCode Confirmed, Logging in with A1 sig...");
|
|
Collection.Scheduler.Cancel(QueryEvent); // cancel query task
|
|
|
|
if (@event.TgtgtKey != null)
|
|
{
|
|
Collection.Keystore.Stub.TgtgtKey = @event.TgtgtKey;
|
|
Collection.Keystore.Session.TempPassword = @event.TempPassword;
|
|
Collection.Keystore.Session.NoPicSig = @event.NoPicSig;
|
|
|
|
_qrCodeTask.SetResult(await DoWtLogin());
|
|
}
|
|
break;
|
|
}
|
|
case TransEmp12.State.CodeExpired:
|
|
{
|
|
Collection.Log.LogWarning(Tag, "QrCode Expired, Please Fetch QrCode Again");
|
|
Collection.Scheduler.Cancel(QueryEvent);
|
|
Collection.Scheduler.Dispose();
|
|
|
|
_qrCodeTask.SetResult(false);
|
|
return;
|
|
}
|
|
case TransEmp12.State.Canceled:
|
|
{
|
|
Collection.Log.LogWarning(Tag, "QrCode Canceled, Please Fetch QrCode Again");
|
|
Collection.Scheduler.Cancel(QueryEvent);
|
|
Collection.Scheduler.Dispose();
|
|
|
|
_qrCodeTask.SetResult(false);
|
|
return;
|
|
}
|
|
case TransEmp12.State.WaitingForConfirm:
|
|
case TransEmp12.State.WaitingForScan:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private async Task BotOnline()
|
|
{
|
|
var onlineEvent = new BotOnlineEvent();
|
|
Collection.Invoker.PostEvent(onlineEvent);
|
|
|
|
var registerEvent = StatusRegisterEvent.Create();
|
|
var registerResponse = await Collection.Business.SendEvent(registerEvent);
|
|
var heartbeatDelegate = new Action(async () => await Collection.Business.PushEvent(SsoAliveEvent.Create()));
|
|
Collection.Log.LogInfo(Tag, $"Register Status: {((StatusRegisterEvent)registerResponse[0]).Message}");
|
|
Collection.Scheduler.Interval("SsoHeartBeat", (int)(4.5 * 60 * 1000), heartbeatDelegate);
|
|
|
|
await Collection.Business.PushEvent(InfoSyncEvent.Create());
|
|
}
|
|
|
|
private async Task<bool> FetchUnusual()
|
|
{
|
|
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.FetchQrCode);
|
|
var result = await Collection.Business.SendEvent(transEmp);
|
|
|
|
if (result.Count != 0)
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Confirmation Request Send");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task QueryUnusualState()
|
|
{
|
|
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.QueryResult);
|
|
var result = await Collection.Business.SendEvent(transEmp);
|
|
|
|
if (result.Count != 0)
|
|
{
|
|
var @event = (TransEmpEvent)result[0];
|
|
var state = (TransEmp12.State)@event.ResultCode;
|
|
Collection.Log.LogInfo(Tag, $"Confirmation State Queried: {state}");
|
|
|
|
switch (state)
|
|
{
|
|
case TransEmp12.State.Confirmed:
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Verification Confirmed, Logging in Unusual Login Service...");
|
|
Collection.Scheduler.Cancel(QueryEvent); // cancel query task
|
|
|
|
if (@event.TempPassword != null) Collection.Keystore.Session.TempPassword = @event.TempPassword;
|
|
_unusualTask.SetResult(await DoUnusualEasyLogin());
|
|
break;
|
|
}
|
|
case TransEmp12.State.CodeExpired:
|
|
{
|
|
Collection.Log.LogWarning(Tag, "Verification Expired, Please Login Again");
|
|
Collection.Scheduler.Cancel(QueryEvent);
|
|
Collection.Scheduler.Dispose();
|
|
|
|
_unusualTask.SetResult(false);
|
|
break;
|
|
}
|
|
case TransEmp12.State.Canceled:
|
|
{
|
|
Collection.Log.LogWarning(Tag, "Verification Canceled, Please Login Again");
|
|
Collection.Scheduler.Cancel(QueryEvent);
|
|
Collection.Scheduler.Dispose();
|
|
|
|
_unusualTask.SetResult(false);
|
|
break;
|
|
}
|
|
case TransEmp12.State.WaitingForConfirm:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<bool> DoUnusualEasyLogin()
|
|
{
|
|
Collection.Log.LogInfo(Tag, "Trying to Login by EasyLogin...");
|
|
var unusualEvent = UnusualEasyLoginEvent.Create();
|
|
var result = await Collection.Business.SendEvent(unusualEvent);
|
|
return result.Count != 0 && ((UnusualEasyLoginEvent)result[0]).Success;
|
|
|
|
}
|
|
|
|
public bool SubmitCaptcha(string ticket, string randStr) => _captchaTask?.TrySetResult((ticket, randStr)) ?? false;
|
|
} |