Skip to content

Commit 236e323

Browse files
committed
Revise the design of Service
1 parent 476ab7f commit 236e323

File tree

8 files changed

+559
-175
lines changed

8 files changed

+559
-175
lines changed

crates/lagrange-core/src/common/app_info.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ impl BotAppInfo {
263263
}
264264
}
265265

266-
fn inner(&self) -> &AppInfo {
266+
pub fn inner(&self) -> &AppInfo {
267267
match self {
268268
Self::Windows(info) | Self::Linux(info) | Self::MacOs(info) => info,
269269
Self::Android { info, .. } => info,

crates/lagrange-core/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pub enum Error {
2323
#[error("IO error: {0}")]
2424
Io(#[from] std::io::Error),
2525

26+
#[error("Packet error: {0}")]
27+
Packet(#[from] crate::utils::binary::PacketError),
28+
2629
#[error("Other error: {0}")]
2730
Other(#[from] anyhow::Error),
2831
}

crates/lagrange-core/src/internal/services.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ impl SsoPacket {
100100
pub mod login;
101101
pub mod message;
102102

103-
pub use login::{LoginEvent, LoginResponse, LoginService};
103+
pub use login::{
104+
Command as LoginCommand, LoginEventReq, LoginEventResp, LoginServicePC,
105+
LoginServiceANDROID, States as LoginStates,
106+
};
104107
pub use message::{SendMessageEvent, SendMessageResponse, SendMessageService};
Lines changed: 253 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,275 @@
1-
use crate::protocol::EncryptType;
2-
use crate::{context::BotContext, protocol::RequestType};
1+
use crate::internal::packets::login::wtlogin::WtLogin;
2+
use crate::{context::BotContext, error::Result};
33
use bytes::Bytes;
44
use lagrange_macros::define_service;
5+
use std::collections::HashMap;
56
use std::sync::Arc;
67

8+
#[allow(unused_imports)] // Used in macro arguments
9+
use crate::protocol::{EncryptType, RequestType, Protocols};
10+
use crate::utils::binary::{BinaryPacket, Prefix};
11+
use crate::utils::crypto::TeaProvider;
12+
13+
/// Command type for login operations
14+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15+
#[repr(u8)]
16+
pub enum Command {
17+
/// TGTGT login command (0x09)
18+
Tgtgt = 0x09,
19+
/// Captcha submission command (0x02)
20+
Captcha = 0x02,
21+
/// Fetch SMS code command (0x08)
22+
FetchSMSCode = 0x08,
23+
/// Submit SMS code command (0x07)
24+
SubmitSMSCode = 0x07,
25+
}
26+
27+
/// Login response states
28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
#[repr(u8)]
30+
pub enum States {
31+
Success = 0,
32+
CaptchaVerify = 2,
33+
SmsRequired = 160,
34+
DeviceLock = 204,
35+
DeviceLockViaSmsNewArea = 239,
36+
PreventByIncorrectPassword = 1,
37+
PreventByReceiveIssue = 3,
38+
PreventByTokenExpired = 15,
39+
PreventByAccountBanned = 40,
40+
PreventByOperationTimeout = 155,
41+
PreventBySmsSentFailed = 162,
42+
PreventByIncorrectSmsCode = 163,
43+
PreventByLoginDenied = 167,
44+
PreventByOutdatedVersion = 235,
45+
PreventByHighRiskOfEnvironment = 237,
46+
Unknown = 240,
47+
}
48+
49+
impl From<u8> for States {
50+
fn from(value: u8) -> Self {
51+
match value {
52+
0 => States::Success,
53+
2 => States::CaptchaVerify,
54+
160 => States::SmsRequired,
55+
204 => States::DeviceLock,
56+
239 => States::DeviceLockViaSmsNewArea,
57+
1 => States::PreventByIncorrectPassword,
58+
3 => States::PreventByReceiveIssue,
59+
15 => States::PreventByTokenExpired,
60+
40 => States::PreventByAccountBanned,
61+
155 => States::PreventByOperationTimeout,
62+
162 => States::PreventBySmsSentFailed,
63+
163 => States::PreventByIncorrectSmsCode,
64+
167 => States::PreventByLoginDenied,
65+
235 => States::PreventByOutdatedVersion,
66+
237 => States::PreventByHighRiskOfEnvironment,
67+
_ => States::Unknown,
68+
}
69+
}
70+
}
71+
72+
/// Unpacks TLV (Tag-Length-Value) data from a binary packet
73+
fn tlv_unpack(reader: &mut BinaryPacket) -> Result<HashMap<u16, Vec<u8>>> {
74+
let mut tlvs = HashMap::new();
75+
76+
let count = reader.read::<u16>()?;
77+
for _ in 0..count {
78+
let tag = reader.read::<u16>()?;
79+
let data = reader.read_bytes_with_prefix(Prefix::INT16)?.to_vec();
80+
tlvs.insert(tag, data);
81+
}
82+
83+
Ok(tlvs)
84+
}
85+
86+
/// Common parsing logic for login responses
87+
fn parse_login_response(
88+
packet: &WtLogin,
89+
input: Bytes,
90+
context: Arc<BotContext>,
91+
ret_code: &mut u8,
92+
error: &mut Option<(String, String)>,
93+
tlvs: &mut HashMap<u16, Vec<u8>>,
94+
) -> Result<()> {
95+
let (command, payload) = packet
96+
.parse(input.as_ref())
97+
.map_err(|e| crate::error::Error::ParseError(e.to_string()))?;
98+
99+
if command != 0x810 {
100+
return Err(crate::error::Error::ParseError(format!(
101+
"Expected command 0x810, got {:#x}",
102+
command
103+
)));
104+
}
105+
106+
let mut reader = BinaryPacket::from_slice(&payload);
107+
let _internal_cmd = reader.read::<u16>()?;
108+
let state = reader.read::<u8>()?;
109+
let mut parsed_tlvs = tlv_unpack(&mut reader)?;
110+
111+
context.log_debug(&format!(
112+
"Login response: state={}, tlvs count={}",
113+
state,
114+
parsed_tlvs.len()
115+
));
116+
117+
*ret_code = state;
118+
119+
// Check for error (TLV 0x146)
120+
if let Some(error_data) = parsed_tlvs.get(&0x146) {
121+
let mut error_reader = BinaryPacket::from_slice(error_data);
122+
let _error_code = error_reader.read::<u32>()?;
123+
let error_title = error_reader.read_string(Prefix::INT16)?;
124+
let error_message = error_reader.read_string(Prefix::INT16)?;
125+
126+
context.log_info(&format!(
127+
"Login error: {} - {}",
128+
error_title, error_message
129+
));
130+
131+
*error = Some((error_title, error_message));
132+
return Ok(());
133+
}
134+
135+
// Check for TLV 0x119 (contains encrypted TLV collection)
136+
if let Some(tgtgt_data) = parsed_tlvs.remove(&0x119) {
137+
let keystore = context.keystore.read().expect("RwLock poisoned");
138+
let tgtgt_key: [u8; 16] = keystore.sigs.tgtgt_key[..16]
139+
.try_into()
140+
.map_err(|_| crate::error::Error::ParseError("Invalid tgtgt_key length".into()))?;
141+
142+
let decrypted = TeaProvider::decrypt(&tgtgt_data, &tgtgt_key)
143+
.map_err(|e| crate::error::Error::ParseError(format!("Failed to decrypt: {}", e)))?;
144+
145+
let mut tlv119_reader = BinaryPacket::from_slice(&decrypted);
146+
let tlv_collection = tlv_unpack(&mut tlv119_reader)?;
147+
148+
context.log_debug(&format!(
149+
"Decrypted TLV 0x119: {} inner TLVs",
150+
tlv_collection.len()
151+
));
152+
153+
*tlvs = tlv_collection;
154+
return Ok(());
155+
}
156+
157+
*tlvs = parsed_tlvs;
158+
Ok(())
159+
}
160+
161+
// Login service with protocol-specific service implementations
7162
define_service! {
8163
LoginService: "wtlogin.login" {
9164
request_type: RequestType::D2Auth,
10165
encrypt_type: EncryptType::EncryptEmpty,
11166

12-
request LoginEvent {
13-
uin: u64,
167+
request LoginEventReq {
168+
cmd: Command,
14169
password: String,
170+
ticket: String,
171+
code: String,
15172
}
16173

17-
response LoginResponse {
18-
success: bool,
19-
message: String,
174+
response LoginEventResp {
175+
ret_code: u8,
176+
error: Option<(String, String)>,
177+
tlvs: HashMap<u16, Vec<u8>>,
20178
}
21179

22-
async fn parse(input: Bytes, context: Arc<BotContext>) -> Result<LoginResponse> {
23-
context.log_debug(&format!("Parsing login response: {} bytes", input.len()));
180+
service(protocol = Protocols::PC) {
181+
async fn parse(input: Bytes, context: Arc<BotContext>) -> Result<LoginEventResp> {
182+
let keystore = context.keystore.read().expect("RwLock poisoned");
183+
let app_info = context.app_info.inner();
184+
let packet = WtLogin::new(&keystore, app_info)
185+
.map_err(|e| crate::error::Error::ParseError(e.to_string()))?;
186+
187+
let mut ret_code = 0;
188+
let mut error = None;
189+
let mut tlvs = HashMap::new();
190+
191+
parse_login_response(&packet, input, context.clone(), &mut ret_code, &mut error, &mut tlvs)?;
192+
193+
Ok(LoginEventResp {
194+
ret_code,
195+
error,
196+
tlvs,
197+
})
198+
}
199+
200+
async fn build(input: LoginEventReq, context: Arc<BotContext>) -> Result<Bytes> {
201+
let keystore = context.keystore.read().expect("RwLock poisoned");
202+
let app_info = context.app_info.inner();
203+
let packet = WtLogin::new(&keystore, app_info)
204+
.map_err(|e| crate::error::Error::BuildError(e.to_string()))?;
24205

25-
let success = !input.is_empty();
26-
let message = if success {
27-
"Login successful".to_string()
28-
} else {
29-
"Login failed".to_string()
30-
};
206+
let data = match input.cmd {
207+
Command::Tgtgt => packet.build_oicq_09(),
208+
_ => {
209+
return Err(crate::error::Error::BuildError(format!(
210+
"Unknown command for PC protocol: {:?}",
211+
input.cmd
212+
)))
213+
}
214+
};
31215

32-
Ok(LoginResponse { success, message })
216+
Ok(Bytes::from(data))
217+
}
33218
}
34219

35-
async fn build(input: LoginEvent, context: Arc<BotContext>) -> Result<Bytes> {
36-
context.log_debug(&format!("Building login request for UIN: {}", input.uin));
220+
service(protocol = Protocols::ANDROID) {
221+
async fn parse(input: Bytes, context: Arc<BotContext>) -> Result<LoginEventResp> {
222+
let keystore = context.keystore.read().expect("RwLock poisoned");
223+
let app_info = context.app_info.inner();
37224

38-
let data = format!("{}:{}", input.uin, input.password);
39-
Ok(Bytes::from(data))
225+
let packet = WtLogin::new(&keystore, app_info)
226+
.map_err(|e| crate::error::Error::ParseError(e.to_string()))?;
227+
228+
let mut ret_code = 0;
229+
let mut error = None;
230+
let mut tlvs = HashMap::new();
231+
232+
parse_login_response(&packet, input, context.clone(), &mut ret_code, &mut error, &mut tlvs)?;
233+
234+
Ok(LoginEventResp {
235+
ret_code,
236+
error,
237+
tlvs,
238+
})
239+
}
240+
241+
async fn build(input: LoginEventReq, context: Arc<BotContext>) -> Result<Bytes> {
242+
let keystore = context.keystore.read().expect("RwLock poisoned");
243+
let app_info = context.app_info.inner();
244+
245+
let packet = WtLogin::new(&keystore, app_info)
246+
.map_err(|e| crate::error::Error::BuildError(e.to_string()))?;
247+
248+
// For Android, we need additional parameters (energy, attach, tlv_548_data)
249+
// These would normally come from the context or be calculated
250+
// For now, we'll use empty arrays as placeholders
251+
let energy = &[];
252+
let attach = &[];
253+
let tlv_548_data = &[];
254+
255+
let data = match input.cmd {
256+
Command::Tgtgt => {
257+
packet.build_oicq_09_android(&input.password, energy, attach, tlv_548_data)
258+
}
259+
Command::Captcha => packet.build_oicq_02_android(&input.ticket, energy, attach),
260+
Command::FetchSMSCode => packet.build_oicq_08_android(attach),
261+
Command::SubmitSMSCode => packet.build_oicq_07_android(&input.code, energy, attach),
262+
};
263+
264+
Ok(Bytes::from(data))
265+
}
40266
}
41267
}
42268
}
269+
270+
// Helper methods for response type
271+
impl LoginEventResp {
272+
pub fn state(&self) -> States {
273+
States::from(self.ret_code)
274+
}
275+
}

crates/lagrange-core/src/internal/services/message.rs

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,40 @@ define_service! {
2222
success: bool,
2323
}
2424

25-
async fn parse(input: Bytes, context: Arc<BotContext>) -> Result<SendMessageResponse> {
26-
context.log_debug(&format!("Parsing send message response: {} bytes", input.len()));
27-
28-
let message_id = u64::from_le_bytes(
29-
input
30-
.get(0..8)
31-
.and_then(|b| b.try_into().ok())
32-
.unwrap_or([0u8; 8]),
33-
);
34-
35-
let time = chrono::Utc::now().timestamp();
36-
37-
Ok(SendMessageResponse {
38-
message_id,
39-
time,
40-
success: true,
41-
})
42-
}
43-
44-
async fn build(input: SendMessageEvent, context: Arc<BotContext>) -> Result<Bytes> {
45-
let msg_type = if input.is_group { "group" } else { "friend" };
46-
context.log_debug(&format!(
47-
"Building {} message to {}: {}",
48-
msg_type, input.target, input.content
49-
));
50-
51-
let data = format!(
52-
"{{\"target\":{},\"content\":\"{}\",\"is_group\":{}}}",
53-
input.target, input.content, input.is_group
54-
);
55-
56-
Ok(Bytes::from(data))
25+
service {
26+
async fn parse(input: Bytes, context: Arc<BotContext>) -> Result<SendMessageResponse> {
27+
context.log_debug(&format!("Parsing send message response: {} bytes", input.len()));
28+
29+
let message_id = u64::from_le_bytes(
30+
input
31+
.get(0..8)
32+
.and_then(|b| b.try_into().ok())
33+
.unwrap_or([0u8; 8]),
34+
);
35+
36+
let time = chrono::Utc::now().timestamp();
37+
38+
Ok(SendMessageResponse {
39+
message_id,
40+
time,
41+
success: true,
42+
})
43+
}
44+
45+
async fn build(input: SendMessageEvent, context: Arc<BotContext>) -> Result<Bytes> {
46+
let msg_type = if input.is_group { "group" } else { "friend" };
47+
context.log_debug(&format!(
48+
"Building {} message to {}: {}",
49+
msg_type, input.target, input.content
50+
));
51+
52+
let data = format!(
53+
"{{\"target\":{},\"content\":\"{}\",\"is_group\":{}}}",
54+
input.target, input.content, input.is_group
55+
);
56+
57+
Ok(Bytes::from(data))
58+
}
5759
}
5860
}
5961
}

0 commit comments

Comments
 (0)