Skip to content

Commit 8b68122

Browse files
PeroSarcyberknight777
authored andcommitted
src: Add admin-only file transfer commands (k.ul, k.dl)
Implement the README TODO admin commands so the configured admin can upload local files and download URLs or replied Telegram media into a local dl/ directory. Signed-off-by: PeroSar <perosar1111@gmail.com>
1 parent ec29a36 commit 8b68122

6 files changed

Lines changed: 294 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
22
config.toml
33
knight-bot.session
4+
dl/

README.org

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ $ cargo run --release
6666
+ =/yaap [device]= - Gets latest YAAP release according to the device.
6767

6868
* Admin commands available currently (Add admin user ID in config.toml)
69+
+ =k.dl [link]= - Download a URL or replied Telegram media to the =dl/= folder.
6970
+ =k.sh [command]= - Execute a shell command.
71+
+ =k.ul [file]= - Upload a file.
7072
+ =k.mot [guid] [carrier] [serial number]= - Gets latest incremental OTA zip from Motorola servers.
7173

7274
** Commands on TODO list
73-
+ =k.ul [file]= - Upload a file.
74-
+ =k.dl [link]= - Download a file.
7575
+ =/qrd= - Decodes a QR Code.
7676
+ =/qrg [text]= - Generates a QR Code.
7777

src/plugins/dl.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//!
2+
//! Copyright (C) 2023-2025 cyberknight777
3+
//!
4+
//! SPDX-License-Identifier: MIT
5+
//!
6+
7+
use grammers_client::{
8+
Client,
9+
media::Media,
10+
message::{InputMessage, Message},
11+
};
12+
use reqwest::Url;
13+
use std::path::{Path, PathBuf};
14+
use tokio::fs;
15+
16+
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
17+
18+
const DL_DIR: &str = "dl";
19+
const DOWNLOAD_FAILED: &str = "<b>Download failed!</b>";
20+
const DOWNLOAD_STARTED: &str = "<b>Downloading file...</b>";
21+
const DOWNLOAD_USAGE: &str = "Reply to a <b>file</b> or give me a <b>proper download link</b>!";
22+
23+
fn filename_from_url(url: &Url) -> String {
24+
url.path_segments()
25+
.and_then(|segments| segments.filter(|segment| !segment.is_empty()).last())
26+
.map(sanitize_filename)
27+
.filter(|name| !name.is_empty())
28+
.unwrap_or_else(|| "download".to_string())
29+
}
30+
31+
fn sanitize_filename(name: &str) -> String {
32+
name.trim()
33+
.trim_matches('.')
34+
.chars()
35+
.map(|ch| match ch {
36+
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_',
37+
ch if ch.is_whitespace() => '_',
38+
_ => ch,
39+
})
40+
.collect()
41+
}
42+
43+
fn unique_path(dir: &Path, filename: &str) -> PathBuf {
44+
let mut path = dir.join(filename);
45+
if !path.exists() {
46+
return path;
47+
}
48+
49+
let source = Path::new(filename);
50+
let stem = source
51+
.file_stem()
52+
.and_then(|value| value.to_str())
53+
.filter(|value| !value.is_empty())
54+
.unwrap_or("download");
55+
let extension = source.extension().and_then(|value| value.to_str());
56+
57+
for index in 1.. {
58+
let candidate = match extension {
59+
Some(extension) => format!("{}-{}.{}", stem, index, extension),
60+
None => format!("{}-{}", stem, index),
61+
};
62+
path = dir.join(candidate);
63+
if !path.exists() {
64+
return path;
65+
}
66+
}
67+
68+
unreachable!()
69+
}
70+
71+
async fn download_path(filename: &str) -> std::result::Result<PathBuf, Box<dyn std::error::Error>> {
72+
let dir = Path::new(DL_DIR);
73+
fs::create_dir_all(dir).await?;
74+
Ok(unique_path(dir, filename))
75+
}
76+
77+
fn filename_from_media(media: &Media) -> String {
78+
match media {
79+
Media::Photo(photo) => format!("photo_{}.jpg", photo.id()),
80+
Media::Document(document) => document
81+
.name()
82+
.map(sanitize_filename)
83+
.filter(|name| !name.is_empty())
84+
.unwrap_or_else(|| format!("document_{}", document.id())),
85+
Media::Sticker(sticker) => sticker
86+
.document
87+
.name()
88+
.map(sanitize_filename)
89+
.filter(|name| !name.is_empty())
90+
.unwrap_or_else(|| format!("sticker_{}.webp", sticker.document.id())),
91+
_ => "download".to_string(),
92+
}
93+
}
94+
95+
async fn download_url(message: &Message, url: Url) -> Result {
96+
let status = message
97+
.reply(InputMessage::new().html(DOWNLOAD_STARTED))
98+
.await?;
99+
100+
let response = match reqwest::get(url.clone()).await {
101+
Ok(response) => response,
102+
Err(_) => {
103+
status
104+
.edit(InputMessage::new().html(DOWNLOAD_FAILED))
105+
.await?;
106+
return Ok(());
107+
}
108+
};
109+
110+
if !response.status().is_success() {
111+
status
112+
.edit(
113+
InputMessage::new().html(format!("{DOWNLOAD_FAILED}\nHTTP {}", response.status())),
114+
)
115+
.await?;
116+
return Ok(());
117+
}
118+
119+
let bytes = match response.bytes().await {
120+
Ok(bytes) => bytes,
121+
Err(_) => {
122+
status
123+
.edit(InputMessage::new().html(DOWNLOAD_FAILED))
124+
.await?;
125+
return Ok(());
126+
}
127+
};
128+
129+
let path = download_path(&filename_from_url(&url)).await?;
130+
fs::write(&path, bytes).await?;
131+
status
132+
.edit(InputMessage::new().html(format!(
133+
"<b>Downloaded:</b> <code>{}</code>",
134+
path.display()
135+
)))
136+
.await?;
137+
138+
return Ok(());
139+
}
140+
141+
async fn download_reply_media(client: Client, message: &Message, media: Media) -> Result {
142+
let status = message
143+
.reply(InputMessage::new().html(DOWNLOAD_STARTED))
144+
.await?;
145+
let path = download_path(&filename_from_media(&media)).await?;
146+
147+
match client.download_media(&media, &path).await {
148+
Ok(_) => {
149+
status
150+
.edit(InputMessage::new().html(format!(
151+
"<b>Downloaded:</b> <code>{}</code>",
152+
path.display()
153+
)))
154+
.await?;
155+
}
156+
Err(_) => {
157+
status
158+
.edit(InputMessage::new().html(DOWNLOAD_FAILED))
159+
.await?;
160+
}
161+
}
162+
163+
return Ok(());
164+
}
165+
166+
pub async fn knightcmd_dl(client: Client, message: &Message, link: String) -> Result {
167+
let link = link.trim();
168+
169+
if link.is_empty() {
170+
if let Some(reply) = client.get_reply_to_message(message).await? {
171+
if let Some(media) = reply.media() {
172+
download_reply_media(client, message, media).await?;
173+
} else {
174+
message
175+
.reply(InputMessage::new().html(DOWNLOAD_USAGE))
176+
.await?;
177+
}
178+
} else {
179+
message
180+
.reply(InputMessage::new().html(DOWNLOAD_USAGE))
181+
.await?;
182+
}
183+
return Ok(());
184+
}
185+
186+
let url = match Url::parse(link) {
187+
Ok(url) if matches!(url.scheme(), "http" | "https") => url,
188+
_ => {
189+
message
190+
.reply(InputMessage::new().html("<b>Invalid download link!</b>"))
191+
.await?;
192+
return Ok(());
193+
}
194+
};
195+
196+
download_url(message, url).await?;
197+
198+
return Ok(());
199+
}

src/plugins/help.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ pub async fn knightcmd_help(message: &Message) -> Result<(), Box<dyn std::error:
2525
if let Some(filename) = entry.file_name().to_str() {
2626
if filename.ends_with(".rs")
2727
&& filename != "mod.rs"
28+
&& filename != "dl.rs"
2829
&& filename != "req.rs"
2930
&& filename != "sh.rs"
3031
&& filename != "mot.rs"
32+
&& filename != "ul.rs"
3133
{
3234
let command_name = filename.trim_end_matches(".rs").to_string();
3335
let description = get_command_description(&command_name, plugin_dir)?;

src/plugins/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::sync::Arc;
1010
mod anyone;
1111
mod aur;
1212
mod cat;
13+
mod dl;
1314
mod dog;
1415
mod eightball;
1516
mod flipcoin;
@@ -34,6 +35,7 @@ mod sh;
3435
mod smsg;
3536
mod start;
3637
mod uid;
38+
mod ul;
3739
mod urb;
3840
mod whois;
3941
mod yaap;
@@ -47,6 +49,7 @@ enum Command {
4749
Anyone,
4850
Aur(String),
4951
Cat(i64),
52+
Dl(String),
5053
Dog(i64),
5154
EightBall,
5255
FlipCoin,
@@ -70,6 +73,7 @@ enum Command {
7073
Smsg(String),
7174
Start,
7275
Uid,
76+
Ul(String),
7377
Urb(String),
7478
Whois(String),
7579
Yaap(String),
@@ -101,6 +105,7 @@ pub async fn handle_msg(client: Client, message: &Message) -> Result {
101105
"/anyone" | "/anyone@ThekNIGHT_bot" => Command::Anyone,
102106
"/aur" | "/aur@ThekNIGHT_bot" => Command::Aur(args.join(" ")),
103107
"/cat" | "/cat@ThekNIGHT_bot" => Command::Cat(args.join(" ").parse().unwrap_or_default()),
108+
"k.dl" => Command::Dl(args.join(" ")),
104109
"/dog" | "/dog@ThekNIGHT_bot" => Command::Dog(args.join(" ").parse().unwrap_or_default()),
105110
"/eightball" | "/eightball@ThekNIGHT_bot" => Command::EightBall,
106111
"/flipcoin" | "/flipcoin@ThekNIGHT_bot" => Command::FlipCoin,
@@ -124,6 +129,7 @@ pub async fn handle_msg(client: Client, message: &Message) -> Result {
124129
"/smsg" | "/smsg@ThekNIGHT_bot" => Command::Smsg(args.join(" ")),
125130
"/start" | "/start@ThekNIGHT_bot" => Command::Start,
126131
"/uid" | "/uid@ThekNIGHT_bot" => Command::Uid,
132+
"k.ul" => Command::Ul(args.join(" ")),
127133
"/urb" | "/urb@ThekNIGHT_bot" => Command::Urb(args.join(" ")),
128134
"/whois" | "/whois@ThekNIGHT_bot" => Command::Whois(args.join(" ")),
129135
"/yaap" | "/yaap@ThekNIGHT_bot" => Command::Yaap(args.join(" ")),
@@ -140,6 +146,7 @@ pub async fn handle_msg(client: Client, message: &Message) -> Result {
140146
Command::Anyone => anyone::knightcmd_anyone(client, message).await?,
141147
Command::Aur(pkg) => aur::knightcmd_aur(message, pkg).await?,
142148
Command::Cat(kat) => cat::knightcmd_cat(client, message, kat).await?,
149+
Command::Dl(link) => dl::knightcmd_dl(client, message, link).await?,
143150
Command::Dog(doge) => dog::knightcmd_dog(client, message, doge).await?,
144151
Command::EightBall => eightball::knightcmd_eightball(client, message).await?,
145152
Command::FlipCoin => flipcoin::knightcmd_flipcoin(client, message).await?,
@@ -165,6 +172,7 @@ pub async fn handle_msg(client: Client, message: &Message) -> Result {
165172
Command::Smsg(stext) => smsg::knightcmd_smsg(client, message, stext).await?,
166173
Command::Start => start::knightcmd_start(message).await?,
167174
Command::Uid => uid::knightcmd_uid(client, message).await?,
175+
Command::Ul(path) => ul::knightcmd_ul(client, message, path).await?,
168176
Command::Urb(word) => urb::knightcmd_urb(message, word).await?,
169177
Command::Whois(site) => whois::knightcmd_whois(message, site).await?,
170178
Command::Yaap(device) => yaap::knightcmd_yaap(client, message, device).await?,
@@ -179,13 +187,18 @@ fn check_msg(message: &Message) -> bool {
179187
&& !message.text().starts_with("/ ")
180188
|| (message.text().ends_with("@ThekNIGHT_bot")
181189
&& !message.text().starts_with("k.sh")
182-
&& !message.text().starts_with("k.mot"));
190+
&& !message.text().starts_with("k.mot")
191+
&& !message.text().starts_with("k.ul")
192+
&& !message.text().starts_with("k.dl"));
183193
}
184194

185195
fn check_cmd(message: &Message, admin_id: i64) -> bool {
186196
return !message.outgoing()
187197
&& (message.sender().and_then(|s| s.id().bare_id()) == Some(admin_id))
188-
&& (message.text().starts_with("k.sh") || message.text().starts_with("k.mot"));
198+
&& (message.text().starts_with("k.sh")
199+
|| message.text().starts_with("k.mot")
200+
|| message.text().starts_with("k.ul")
201+
|| message.text().starts_with("k.dl"));
189202
}
190203

191204
pub fn random(modulo: u8) -> u8 {

src/plugins/ul.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//!
2+
//! Copyright (C) 2023-2025 cyberknight777
3+
//!
4+
//! SPDX-License-Identifier: MIT
5+
//!
6+
7+
use grammers_client::{
8+
Client,
9+
message::{InputMessage, Message},
10+
};
11+
use std::{
12+
env,
13+
path::{Path, PathBuf},
14+
};
15+
16+
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
17+
18+
fn resolve_upload_path(path: &str) -> std::io::Result<PathBuf> {
19+
let path = Path::new(path);
20+
let path = if path.is_absolute() {
21+
path.to_path_buf()
22+
} else {
23+
env::current_dir()?.join(path)
24+
};
25+
26+
path.canonicalize()
27+
}
28+
29+
pub async fn knightcmd_ul(client: Client, message: &Message, path: String) -> Result {
30+
let path = path.trim();
31+
32+
if path.is_empty() {
33+
message
34+
.reply(InputMessage::new().html("Give me a <b>proper file path</b> to upload!"))
35+
.await?;
36+
return Ok(());
37+
}
38+
39+
let file_path = match resolve_upload_path(path) {
40+
Ok(path) => path,
41+
Err(_) => {
42+
message
43+
.reply(InputMessage::new().html("<b>File not found!</b>"))
44+
.await?;
45+
return Ok(());
46+
}
47+
};
48+
49+
if !file_path.is_file() {
50+
message
51+
.reply(InputMessage::new().html("<b>File not found!</b>"))
52+
.await?;
53+
return Ok(());
54+
}
55+
56+
let status = message
57+
.reply(InputMessage::new().html("<b>Uploading file...</b>"))
58+
.await?;
59+
60+
match client.upload_file(file_path).await {
61+
Ok(file) => {
62+
status.delete().await?;
63+
message
64+
.reply(InputMessage::new().text("").file(file))
65+
.await?;
66+
}
67+
Err(_) => {
68+
status
69+
.edit(InputMessage::new().html("<b>Upload failed!</b>"))
70+
.await?;
71+
}
72+
}
73+
74+
return Ok(());
75+
}

0 commit comments

Comments
 (0)