From 0afec8742388cd46a5095dfdc56d61c415a3e954 Mon Sep 17 00:00:00 2001 From: ooopus Date: Sat, 17 Jan 2026 22:16:59 +0900 Subject: [PATCH 1/6] Add methods to check user subscription status and fake account flag --- grammers-client/src/peer/user.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/grammers-client/src/peer/user.rs b/grammers-client/src/peer/user.rs index 7fe024d0..1d0feb67 100644 --- a/grammers-client/src/peer/user.rs +++ b/grammers-client/src/peer/user.rs @@ -278,6 +278,16 @@ impl User { self.user().map(|u| u.scam).unwrap_or(false) } + /// Does this user have a Telegram Premium subscription? + pub fn is_premium(&self) -> bool { + self.user().map(|u| u.premium).unwrap_or(false) + } + + /// Has this user been flagged as a fake account? + pub fn fake(&self) -> bool { + self.user().map(|u| u.fake).unwrap_or(false) + } + /// The reason(s) why this user is restricted, could be empty. pub fn restriction_reason(&self) -> Vec { if let Some(reasons) = self.user().and_then(|u| u.restriction_reason.as_ref()) { From e4f048c54d0326ba27eb2b6b5056c6652a2b322d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 07:07:10 +0000 Subject: [PATCH 2/6] Initial plan From ae9f6d7f791b860fece27b7f32f89c5cafacc8fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 07:09:35 +0000 Subject: [PATCH 3/6] Add requires_join_request methods to Channel and Group Co-authored-by: ooopus <107778929+ooopus@users.noreply.github.com> --- grammers-client/src/peer/channel.rs | 5 +++++ grammers-client/src/peer/group.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/grammers-client/src/peer/channel.rs b/grammers-client/src/peer/channel.rs index 633e282d..02a00cdb 100644 --- a/grammers-client/src/peer/channel.rs +++ b/grammers-client/src/peer/channel.rs @@ -214,6 +214,11 @@ impl Channel { None => None, } } + + /// Return whether this channel requires join requests. + pub fn requires_join_request(&self) -> bool { + self.raw.join_request + } } impl TryFrom for ChannelKind { diff --git a/grammers-client/src/peer/group.rs b/grammers-client/src/peer/group.rs index 4570f913..e0589d02 100644 --- a/grammers-client/src/peer/group.rs +++ b/grammers-client/src/peer/group.rs @@ -201,6 +201,18 @@ impl Group { C::Channel(_) | C::ChannelForbidden(_) => true, } } + + /// Return whether this group requires join requests. + pub fn requires_join_request(&self) -> bool { + use tl::enums::Chat; + + match &self.raw { + Chat::Empty(_) | Chat::Chat(_) | Chat::Forbidden(_) | Chat::ChannelForbidden(_) => { + false + } + Chat::Channel(channel) => channel.join_request, + } + } } impl From for PeerInfo { From 4039c96efeb4828f23616e9ff608051dfc16c4d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:40:04 +0000 Subject: [PATCH 4/6] Initial plan From 5d387b6e72073a44be218e585040e3eaac81fcbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:46:17 +0000 Subject: [PATCH 5/6] Add HTTP CONNECT and SOCKS4 proxy support - Added base64 dependency to proxy feature in Cargo.toml - Implemented SOCKS4 proxy support with IPv4-only limitation - Implemented HTTP/HTTPS CONNECT tunnel with optional basic auth - Both protocols reuse NetStream::Tcp after tunnel establishment Co-authored-by: ooopus <107778929+ooopus@users.noreply.github.com> --- grammers-mtsender/Cargo.toml | 3 +- grammers-mtsender/src/net/tcp.rs | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/grammers-mtsender/Cargo.toml b/grammers-mtsender/Cargo.toml index 30c46bd9..52723e55 100644 --- a/grammers-mtsender/Cargo.toml +++ b/grammers-mtsender/Cargo.toml @@ -14,7 +14,7 @@ categories = ["api-bindings", "network-programming"] edition = "2024" [features] -proxy = ["tokio-socks", "hickory-resolver", "url"] +proxy = ["tokio-socks", "hickory-resolver", "url", "base64"] [dependencies] bytes = "1.10.1" @@ -29,6 +29,7 @@ tokio = { version = "1.47.1", default-features = false, features = ["io-util", " tokio-socks = { version = "0.5.2", optional = true } hickory-resolver = { version = "0.25.2", optional = true } url = { version = "2.5.7", optional = true } +base64 = { version = "0.22", optional = true } [dev-dependencies] simple_logger = { version = "5.0.0", default-features = false, features = ["colors"] } diff --git a/grammers-mtsender/src/net/tcp.rs b/grammers-mtsender/src/net/tcp.rs index 76845d40..f4976ed0 100644 --- a/grammers-mtsender/src/net/tcp.rs +++ b/grammers-mtsender/src/net/tcp.rs @@ -96,6 +96,91 @@ impl NetStream { )) } } + "socks4" => { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let mut stream = TcpStream::connect(socks_addr).await?; + + // SOCKS4 CONNECT: VER(04) CMD(01) DSTPORT(2B) DSTIP(4B) USERID NULL + let ip = match addr.ip() { + std::net::IpAddr::V4(v4) => v4, + _ => { + return Err(io::Error::new( + ErrorKind::InvalidInput, + "SOCKS4 does not support IPv6", + )) + } + }; + let mut req = vec![0x04, 0x01]; + req.extend_from_slice(&addr.port().to_be_bytes()); + req.extend_from_slice(&ip.octets()); + // userid(可选,用 username) + req.extend_from_slice(username.as_bytes()); + req.push(0x00); // null terminator + + stream.write_all(&req).await?; + + // 响应 8 字节: VN(00) CD DSTPORT(2B) DSTIP(4B) + let mut resp = [0u8; 8]; + stream.read_exact(&mut resp).await?; + + if resp[1] != 0x5A { + return Err(io::Error::new( + ErrorKind::ConnectionAborted, + format!("SOCKS4 CONNECT rejected (code: 0x{:02X})", resp[1]), + )); + } + + Ok(NetStream::Tcp(stream)) + } + "http" | "https" => { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use base64::Engine; + + let mut stream = TcpStream::connect(socks_addr).await?; + + let target = format!("{}:{}", addr.ip(), addr.port()); + let mut request = format!("CONNECT {} HTTP/1.1\r\nHost: {}\r\n", target, target); + + if !username.is_empty() { + let creds = format!("{}:{}", username, password); + let encoded = base64::engine::general_purpose::STANDARD + .encode(creds.as_bytes()); + request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded)); + } + request.push_str("\r\n"); + stream.write_all(request.as_bytes()).await?; + + // 逐字节读响应头直到 \r\n\r\n + let mut buf = Vec::with_capacity(512); + let mut byte = [0u8; 1]; + loop { + stream.read_exact(&mut byte).await?; + buf.push(byte[0]); + if buf.ends_with(b"\r\n\r\n") { + break; + } + if buf.len() > 4096 { + return Err(io::Error::new( + ErrorKind::InvalidData, + "HTTP CONNECT response too large", + )); + } + } + + let response = String::from_utf8_lossy(&buf); + let status_line = response.lines().next().unwrap_or(""); + let parts: Vec<&str> = status_line.splitn(3, ' ').collect(); + if parts.len() < 2 || parts[1] != "200" { + return Err(io::Error::new( + ErrorKind::ConnectionAborted, + format!("HTTP CONNECT failed: {}", status_line), + )); + } + + // 隧道建立后底层 TCP 等同直连 + Ok(NetStream::Tcp(stream)) + } scheme => Err(io::Error::new( ErrorKind::ConnectionAborted, format!("proxy scheme not supported: {}", scheme), From 0aa24b24a4e0bfff4e8c04cda595bb5efba23014 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:50:58 +0000 Subject: [PATCH 6/6] Improve HTTP CONNECT error handling - Changed unwrap_or to explicit error for missing status line - Makes error handling more clear when HTTP response is malformed Co-authored-by: ooopus <107778929+ooopus@users.noreply.github.com> --- grammers-mtsender/src/net/tcp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grammers-mtsender/src/net/tcp.rs b/grammers-mtsender/src/net/tcp.rs index f4976ed0..76ed2b53 100644 --- a/grammers-mtsender/src/net/tcp.rs +++ b/grammers-mtsender/src/net/tcp.rs @@ -169,7 +169,9 @@ impl NetStream { } let response = String::from_utf8_lossy(&buf); - let status_line = response.lines().next().unwrap_or(""); + let status_line = response.lines().next().ok_or_else(|| { + io::Error::new(ErrorKind::InvalidData, "HTTP CONNECT response has no status line") + })?; let parts: Vec<&str> = status_line.splitn(3, ' ').collect(); if parts.len() < 2 || parts[1] != "200" { return Err(io::Error::new(