netget/client/torrent_tracker/
actions.rs1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static TRACKER_ANNOUNCE_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "tracker_announce_response",
18 "Received announce response from BitTorrent tracker",
19 json!({
20 "type": "disconnect"
21 }),
22 )
23 .with_parameters(vec![
24 Parameter {
25 name: "interval".to_string(),
26 type_hint: "number".to_string(),
27 description: "Time in seconds to wait before next announce".to_string(),
28 required: false,
29 },
30 Parameter {
31 name: "complete".to_string(),
32 type_hint: "number".to_string(),
33 description: "Number of seeders".to_string(),
34 required: false,
35 },
36 Parameter {
37 name: "incomplete".to_string(),
38 type_hint: "number".to_string(),
39 description: "Number of leechers".to_string(),
40 required: false,
41 },
42 Parameter {
43 name: "peers".to_string(),
44 type_hint: "string".to_string(),
45 description: "Peer list from tracker".to_string(),
46 required: false,
47 },
48 ])
49});
50
51pub static TRACKER_SCRAPE_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
53 EventType::new(
54 "tracker_scrape_response",
55 "Received scrape response from BitTorrent tracker",
56 json!({
57 "type": "disconnect"
58 }),
59 )
60 .with_parameters(vec![Parameter {
61 name: "files".to_string(),
62 type_hint: "string".to_string(),
63 description: "File statistics from tracker".to_string(),
64 required: false,
65 }])
66});
67
68pub struct TorrentTrackerClientProtocol;
70
71impl TorrentTrackerClientProtocol {
72 pub fn new() -> Self {
73 Self
74 }
75}
76
77impl Protocol for TorrentTrackerClientProtocol {
79 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
80 vec![
81 ActionDefinition {
82 name: "tracker_announce".to_string(),
83 description: "Announce to the BitTorrent tracker".to_string(),
84 parameters: vec![
85 Parameter {
86 name: "info_hash".to_string(),
87 type_hint: "string".to_string(),
88 description: "The torrent's 20-byte info_hash. Percent-encoding is \
89 applied for you, so send it however you have it \
90 (40 hex characters is the usual form); a value that \
91 is already percent-encoded is also accepted."
92 .to_string(),
93 required: true,
94 },
95 Parameter {
96 name: "peer_id".to_string(),
97 type_hint: "string".to_string(),
98 description: "Our 20-byte peer id, e.g. \"-NG0001-abcdefghijkl\". \
99 Percent-encoding is applied for you."
100 .to_string(),
101 required: true,
102 },
103 Parameter {
104 name: "port".to_string(),
105 type_hint: "number".to_string(),
106 description: "Port number for peer connections".to_string(),
107 required: true,
108 },
109 Parameter {
110 name: "uploaded".to_string(),
111 type_hint: "number".to_string(),
112 description: "Bytes uploaded".to_string(),
113 required: false,
114 },
115 Parameter {
116 name: "downloaded".to_string(),
117 type_hint: "number".to_string(),
118 description: "Bytes downloaded".to_string(),
119 required: false,
120 },
121 Parameter {
122 name: "left".to_string(),
123 type_hint: "number".to_string(),
124 description: "Bytes still to download. BEP 3 reads 0 as \"I have the \
125 complete torrent\", which is what makes a tracker \
126 treat us as a seeder -- so send a real figure. Omit \
127 it and no completeness is claimed."
128 .to_string(),
129 required: false,
130 },
131 Parameter {
132 name: "event".to_string(),
133 type_hint: "string".to_string(),
134 description: "Event type: started, completed, stopped".to_string(),
135 required: false,
136 },
137 ],
138 example: json!({
139 "type": "tracker_announce",
140 "info_hash": "%12%34%56%78%90%AB%CD%EF%12%34%56%78%90%AB%CD%EF%12%34%56%78",
141 "peer_id": "-TR2940-abcdefghijkl",
142 "port": 6881,
143 "uploaded": 0,
144 "downloaded": 0,
145 "left": 1024000,
146 "event": "started"
147 }),
148 log_template: None,
149 },
150 ActionDefinition {
151 name: "tracker_scrape".to_string(),
152 description: "Scrape statistics from the BitTorrent tracker".to_string(),
153 parameters: vec![Parameter {
154 name: "info_hash".to_string(),
155 type_hint: "string".to_string(),
156 description: "The torrent's 20-byte info_hash to scrape. \
157 Percent-encoding is applied for you."
158 .to_string(),
159 required: true,
160 }],
161 example: json!({
162 "type": "tracker_scrape",
163 "info_hash": "%12%34%56%78%90%AB%CD%EF%12%34%56%78%90%AB%CD%EF%12%34%56%78"
164 }),
165 log_template: None,
166 },
167 ActionDefinition {
168 name: "disconnect".to_string(),
169 description: "Disconnect from the tracker".to_string(),
170 parameters: vec![],
171 example: json!({
172 "type": "disconnect"
173 }),
174 log_template: None,
175 },
176 ]
177 }
178 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
179 vec![]
180 }
181 fn protocol_name(&self) -> &'static str {
182 "BitTorrent Tracker"
183 }
184 fn get_event_types(&self) -> Vec<EventType> {
193 vec![
194 TRACKER_ANNOUNCE_RESPONSE_EVENT.clone(),
195 TRACKER_SCRAPE_RESPONSE_EVENT.clone(),
196 ]
197 }
198 fn stack_name(&self) -> &'static str {
199 "ETH>IP>TCP>HTTP>BitTorrent-Tracker"
200 }
201 fn keywords(&self) -> Vec<&'static str> {
202 vec![
203 "bittorrent",
204 "tracker",
205 "torrent tracker",
206 "announce",
207 "scrape",
208 ]
209 }
210 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
211 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
212
213 ProtocolMetadataV2::builder()
214 .state(DevelopmentState::Experimental)
215 .implementation("HTTP GET with bencode response parsing")
216 .llm_control("Full control over tracker announces and scrapes")
217 .e2e_testing("Mock tracker server")
218 .build()
219 }
220 fn description(&self) -> &'static str {
221 "BitTorrent tracker client for peer discovery"
222 }
223 fn example_prompt(&self) -> &'static str {
224 "Connect to tracker at http://tracker.example.com:6969/announce and announce with info_hash xyz"
225 }
226 fn group_name(&self) -> &'static str {
227 "P2P"
228 }
229 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
230 use crate::llm::actions::StartupExamples;
231 use serde_json::json;
232
233 StartupExamples::new(
234 json!({
236 "type": "open_client",
237 "remote_addr": "tracker.example.com:6969",
238 "base_stack": "torrent-tracker",
239 "instruction": "Announce to the tracker with info_hash and get peer list"
240 }),
241 json!({
243 "type": "open_client",
244 "remote_addr": "tracker.example.com:6969",
245 "base_stack": "torrent-tracker",
246 "event_handlers": [{
247 "event_pattern": "tracker_announce_response",
248 "handler": {
249 "type": "script",
250 "language": "python",
251 "code": "<tracker_client_handler>"
252 }
253 }]
254 }),
255 json!({
257 "type": "open_client",
258 "remote_addr": "tracker.example.com:6969",
259 "base_stack": "torrent-tracker",
260 "event_handlers": [{
261 "event_pattern": "tracker_announce_response",
262 "handler": {
263 "type": "static",
264 "actions": [{
265 "type": "disconnect"
266 }]
267 }
268 }]
269 }),
270 )
271 }
272}
273
274impl Client for TorrentTrackerClientProtocol {
276 fn connect(
277 &self,
278 ctx: crate::protocol::ConnectContext,
279 ) -> std::pin::Pin<
280 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
281 > {
282 Box::pin(async move {
283 use crate::client::torrent_tracker::TorrentTrackerClient;
284 TorrentTrackerClient::connect_with_llm_actions(
285 ctx.remote_addr,
286 ctx.llm_client,
287 ctx.state,
288 ctx.status_tx,
289 ctx.client_id,
290 )
291 .await
292 })
293 }
294 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
295 let action_type = action
296 .get("type")
297 .and_then(|v| v.as_str())
298 .context("Missing 'type' field in action")?;
299
300 match action_type {
301 "tracker_announce" => Ok(ClientActionResult::Custom {
302 name: "tracker_announce".to_string(),
303 data: action,
304 }),
305 "tracker_scrape" => Ok(ClientActionResult::Custom {
306 name: "tracker_scrape".to_string(),
307 data: action,
308 }),
309 "disconnect" => Ok(ClientActionResult::Disconnect),
310 _ => Err(anyhow::anyhow!(
311 "Unknown Tracker client action: {}",
312 action_type
313 )),
314 }
315 }
316}