use std::error::Error;
use std::future::Future;
use std::{
backtrace::Backtrace,
path::{Path, PathBuf},
sync::Arc,
};
use bytes::Bytes;
use futures_util::StreamExt;
use ricq::{
client::{Connector, DefaultConnector, NetworkStatus, Token},
ext::{common::after_login, reconnect::fast_login},
handler::DefaultHandler,
version::get_version,
Device, LoginDeviceLocked, LoginNeedCaptcha, LoginResponse, LoginSuccess,
};
use thiserror::Error;
use tokio::task::JoinHandle;
use tokio_util::codec::{FramedRead, LinesCodec};
use crate::{client::Client, utils::retry};
box_error_impl!(LoginError, LoginErrorImpl, "登录错误。");
pub use ricq::Protocol;
#[derive(Error, Debug)]
enum LoginErrorImpl {
#[error("`device.json` 错误")]
DeviceError(#[from] crate::device::DeviceError),
#[error("无效的 token 文件")]
InvalidToken {
#[from]
source: serde_json::Error,
backtrace: Backtrace,
},
#[error("IO 错误")]
IOError {
#[from]
source: std::io::Error,
backtrace: Backtrace,
},
#[error("服务器返回意外响应: {response:?}")]
RemoteException {
response: LoginResponse,
backtrace: Backtrace,
},
#[error("登录失败")]
RQError {
#[from]
source: ricq::RQError,
backtrace: Backtrace,
},
#[error("需要设备锁验证 ({message}), 请前往链接进行验证: {url}")]
DeviceLocked {
message: String,
url: String,
},
#[error("账号被冻结")]
AccountFrozen,
#[error("短信请求过于频繁")]
TooManySMSRequest,
#[error("二维码已取消")]
QrCodeCancelled,
#[error("连接断开")]
ConnectionClosed {
#[from]
source: tokio::task::JoinError,
backtrace: Backtrace,
},
#[error("重连终止")]
ReconnectAborted {
message: String,
backtrace: Backtrace,
},
#[error("其他错误")]
Other {
#[from]
source: Box<(dyn Error + Sync + Send)>,
backtrace: Backtrace,
},
}
type Result<T> = std::result::Result<T, LoginError>;
pub struct AliveHandle {
client: Arc<ricq::Client>,
account_data_folder: PathBuf,
alive: Option<JoinHandle<()>>,
}
impl AliveHandle {
pub(crate) fn new(
client: Arc<ricq::Client>,
account_data_folder: PathBuf,
alive: JoinHandle<()>,
) -> Self {
Self {
client,
account_data_folder,
alive: Some(alive),
}
}
pub async fn alive(&mut self) -> Result<()> {
if let Some(alive) = self.alive.take() {
alive.await?;
}
Ok(())
}
pub async fn reconnect(&mut self) -> Result<()> {
if self.alive.is_none() {
let handle = reconnect(&self.client, &self.account_data_folder).await?;
self.alive = Some(handle);
}
Ok(())
}
pub async fn auto_reconnect(mut self) -> Result<()> {
loop {
self.alive().await?;
self.reconnect().await?;
}
}
}
async fn login_impl<Fut>(
uin: i64,
protocol: Protocol,
data_folder: impl AsRef<Path>,
login_with_credential: impl FnOnce(Arc<ricq::Client>) -> Fut,
) -> Result<(Arc<Client>, AliveHandle)>
where
Fut: Future<Output = Result<()>>,
{
let account_data_folder = data_folder.as_ref().join(uin.to_string());
tokio::fs::create_dir_all(&account_data_folder).await?;
let device = load_device_json(uin, &account_data_folder).await?;
let (client, alive) = prepare_client(device, protocol).await?;
if !try_token_login(&client, &account_data_folder).await? {
login_with_credential(client.clone()).await?;
}
after_login(&client).await;
save_token(&client, &account_data_folder).await?;
let alive = AliveHandle::new(client.clone(), account_data_folder, alive);
let client = Arc::new(Client::new(client).await);
Ok((client, alive))
}
pub async fn login_with_password(
uin: i64,
password: &str,
protocol: Protocol,
data_folder: impl AsRef<Path>,
) -> Result<(Arc<Client>, AliveHandle)> {
login_impl(uin, protocol, data_folder, move |client| async move {
let resp = client.password_login(uin, password).await?;
handle_password_login_resp(&client, resp).await?;
Ok(())
})
.await
}
pub async fn login_with_password_md5(
uin: i64,
password_md5: &[u8],
protocol: Protocol,
data_folder: impl AsRef<Path>,
) -> Result<(Arc<Client>, AliveHandle)> {
login_impl(uin, protocol, data_folder, move |client| async move {
let resp = client.password_md5_login(uin, password_md5).await?;
handle_password_login_resp(&client, resp).await?;
Ok(())
})
.await
}
pub async fn login_with_qrcode(
uin: i64,
show_qrcode: impl FnMut(Bytes) -> std::result::Result<(), Box<dyn Error + Send + Sync>>,
data_folder: impl AsRef<Path>,
) -> Result<(Arc<Client>, AliveHandle)> {
login_impl(
uin,
Protocol::AndroidWatch,
data_folder,
move |client| async move {
qrcode_login(&client, uin, show_qrcode).await?;
Ok(())
},
)
.await
}
#[macro_export]
macro_rules! login {
($uin: expr, password = $password: expr, protocol = $protocol: expr, data_folder = $data_folder: expr $(,)?) => {
$crate::login::login_with_password($uin, $password, $protocol, $data_folder)
};
($uin: expr, password = $password: expr, protocol = $protocol: expr $(,)?) => {
$crate::login::login_with_password(
$uin,
$password,
$protocol,
::std::path::Path::new("./bots"),
)
};
($uin: expr, password_md5 = $password_md5: expr, protocol = $protocol: expr, data_folder = $data_folder: expr $(,)?) => {
$crate::login::login_with_password_md5($uin, $password_md5, $protocol, $data_folder)
};
($uin: expr, password_md5 = $password_md5: expr, protocol = $protocol: expr $(,)?) => {
$crate::login::login_with_password_md5(
$uin,
$password_md5,
$protocol,
::std::path::Path::new("./bots"),
)
};
($uin: expr, show_qrcode = $show_qrcode: expr, data_folder = $data_folder: expr $(,)?) => {
$crate::login::login_with_qrcode($uin, $show_qrcode, $data_folder)
};
($uin: expr, show_qrcode = $show_qrcode: expr $(,)?) => {
$crate::login::login_with_qrcode($uin, $show_qrcode, ::std::path::Path::new("./bots"))
};
}
async fn load_device_json(uin: i64, data_folder: impl AsRef<Path>) -> Result<Device> {
use crate::device;
let device_json = data_folder.as_ref().join("device.json");
let device = if device_json.exists() {
let json = tokio::fs::read_to_string(device_json).await?;
device::from_json(&json, &device::random_from_uin(uin))?
} else {
let device = device::random_from_uin(uin);
let json = device::to_json(&device)?;
tokio::fs::write(device_json, json).await?;
device
};
Ok(device)
}
async fn prepare_client(
device: Device,
protocol: Protocol,
) -> tokio::io::Result<(Arc<ricq::Client>, JoinHandle<()>)> {
let client = Arc::new(ricq::Client::new(
device,
get_version(protocol),
DefaultHandler, ));
let alive = tokio::spawn({
let client = client.clone();
let stream = DefaultConnector.connect(&client).await?;
async move { client.start(stream).await }
});
tokio::task::yield_now().await; Ok((client, alive))
}
async fn try_token_login(
client: &ricq::Client,
account_data_folder: impl AsRef<Path>,
) -> Result<bool> {
let token_path = account_data_folder.as_ref().join("token.json");
if !token_path.exists() {
return Ok(false);
}
tracing::info!("发现上一次登录的 token,尝试使用 token 登录");
let token = tokio::fs::read_to_string(&token_path).await?;
let token: Token = serde_json::from_str(&token)?;
match client.token_login(token).await {
Ok(login_resp) => {
if let LoginResponse::Success(LoginSuccess {
ref account_info, ..
}) = login_resp
{
tracing::info!("登录成功: {:?}", account_info);
return Ok(true);
}
Err(LoginErrorImpl::RemoteException {
response: login_resp,
backtrace: Backtrace::capture(),
}
.into())
}
Err(_) => {
tracing::info!("token 登录失败,将删除 token");
tokio::fs::remove_file(token_path).await?;
Ok(false)
}
}
}
async fn save_token(client: &ricq::Client, account_data_folder: impl AsRef<Path>) -> Result<()> {
let token = client.gen_token().await;
let token = serde_json::to_string(&token)?;
let token_path = account_data_folder.as_ref().join("token.json");
tokio::fs::write(token_path, token).await?;
Ok(())
}
async fn handle_password_login_resp(client: &ricq::Client, mut resp: LoginResponse) -> Result<()> {
loop {
match resp {
LoginResponse::Success(LoginSuccess {
ref account_info, ..
}) => {
tracing::info!("登录成功: {:?}", account_info);
break;
}
LoginResponse::DeviceLocked(LoginDeviceLocked {
verify_url,
message,
..
}) => {
return Err(LoginErrorImpl::DeviceLocked {
message: message.unwrap_or_default(),
url: verify_url.unwrap_or_default(),
}
.into());
}
LoginResponse::NeedCaptcha(LoginNeedCaptcha { ref verify_url, .. }) => {
tracing::info!("滑块 url: {}", verify_url.as_deref().unwrap_or("")); tracing::info!("请输入 ticket:");
let mut reader = FramedRead::new(tokio::io::stdin(), LinesCodec::new());
let ticket = reader.next().await.transpose().unwrap().unwrap();
resp = client.submit_ticket(&ticket).await?;
}
LoginResponse::DeviceLockLogin { .. } => {
resp = client.device_lock_login().await?;
}
LoginResponse::AccountFrozen => return Err(LoginErrorImpl::AccountFrozen.into()),
LoginResponse::TooManySMSRequest => {
return Err(LoginErrorImpl::TooManySMSRequest.into())
}
unknown => {
return Err(LoginErrorImpl::RemoteException {
response: unknown,
backtrace: Backtrace::capture(),
}
.into())
}
}
}
Ok(())
}
pub async fn qrcode_login(
client: &ricq::Client,
uin: i64,
mut show_qrcode: impl FnMut(Bytes) -> std::result::Result<(), Box<dyn Error + Send + Sync>>,
) -> Result<()> {
use std::time::Duration;
use ricq::{QRCodeConfirmed, QRCodeImageFetch, QRCodeState};
tracing::info!("使用二维码登录,uin={}", uin);
let mut resp = client.fetch_qrcode().await?;
let mut image_sig = bytes::Bytes::new();
loop {
match resp {
QRCodeState::ImageFetch(QRCodeImageFetch {
image_data,
ref sig,
}) => {
show_qrcode(image_data)?;
image_sig = sig.clone();
}
QRCodeState::WaitingForScan => {
tracing::debug!("等待二维码扫描")
}
QRCodeState::WaitingForConfirm => {
tracing::debug!("二维码已扫描,等待确认")
}
QRCodeState::Timeout => {
tracing::info!("二维码已超时,重新获取");
if let QRCodeState::ImageFetch(QRCodeImageFetch {
image_data,
ref sig,
}) = client.fetch_qrcode().await.expect("failed to fetch qrcode")
{
show_qrcode(image_data)?;
image_sig = sig.clone();
}
}
QRCodeState::Confirmed(QRCodeConfirmed {
ref tmp_pwd,
ref tmp_no_pic_sig,
ref tgt_qr,
..
}) => {
tracing::info!("二维码已确认");
let mut login_resp = client.qrcode_login(tmp_pwd, tmp_no_pic_sig, tgt_qr).await?;
if let LoginResponse::DeviceLockLogin { .. } = login_resp {
login_resp = client.device_lock_login().await?;
}
if let LoginResponse::Success(LoginSuccess {
ref account_info, ..
}) = login_resp
{
tracing::info!("登录成功: {:?}", account_info);
let real_uin = client.uin().await;
if real_uin != uin {
tracing::warn!("预期登录账号 {},但实际登陆账号为 {}", uin, real_uin);
}
break;
}
return Err(LoginErrorImpl::RemoteException {
response: login_resp,
backtrace: Backtrace::capture(),
}
.into());
}
QRCodeState::Canceled => return Err(LoginErrorImpl::QrCodeCancelled.into()),
}
tokio::time::sleep(Duration::from_secs(5)).await;
resp = client.query_qrcode_result(&image_sig).await?;
}
Ok(())
}
pub(crate) async fn reconnect(
client: &Arc<ricq::Client>,
account_data_folder: &Path,
) -> Result<JoinHandle<()>> {
retry(
10,
|| async {
if client.get_status() != (NetworkStatus::NetworkOffline as u8) {
return Ok(Err(LoginErrorImpl::ReconnectAborted {
message: "客户端因非网络原因下线,不再重连".to_string(),
backtrace: Backtrace::capture(),
}
.into()));
}
client.stop(NetworkStatus::NetworkOffline);
tracing::error!("客户端连接中断,将在 10 秒后重连");
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
let alive = tokio::spawn({
let client = client.clone();
let stream = DefaultConnector.connect(&client).await?;
async move { client.start(stream).await }
});
tokio::task::yield_now().await;
let token_path = account_data_folder.join("token.json");
if !token_path.exists() {
return Ok(Err(LoginErrorImpl::ReconnectAborted {
message: "重连失败:无法找到上次登录的 token".to_string(),
backtrace: Backtrace::capture(),
}
.into()));
}
let token = tokio::fs::read_to_string(token_path).await?;
let token = match serde_json::from_str(&token) {
Ok(token) => token,
Err(err) => {
return Ok(Err(LoginErrorImpl::ReconnectAborted {
message: format!("重连失败:无法解析上次登录的 token: {err}"),
backtrace: Backtrace::capture(),
}
.into()));
}
};
fast_login(client, &ricq::ext::reconnect::Credential::Token(token))
.await
.map_err(|e| {
client.stop(NetworkStatus::NetworkOffline);
e
})?;
after_login(client).await;
tracing::info!("客户端重连成功");
Ok(Ok(alive))
},
|e: LoginError, c| async move {
tracing::error!("客户端重连失败,原因:{},剩余尝试 {} 次", e, c);
if let Some(backtrace) = (&e as &dyn Error).request_ref::<Backtrace>() {
tracing::debug!("backtrace: {}", backtrace);
}
},
)
.await?
}