1use crate::client::args::Args;
22use crate::client::client_storage::client_storage::{ClientStorage, BUCKETS, BUCKET_TRIMS};
23use crate::client::key_locker::key_locker::KeyLocker;
24use crate::client::meta_post::meta_post::MetaPost;
25use crate::client::meta_post::meta_post_manager::MetaPostManager;
26use crate::client::peer_tracker::peer_tracker::{PeerTracker, SharedPeerTracker};
27use crate::client::post_bundle::live_post_bundle_manager::LivePostBundleManager;
28use crate::client::post_bundle::post_bundle_manager::PostBundleManager;
29use crate::client::post_bundle::posting;
30use crate::client::timeline::recent_posts_pen::RecentPostsPen;
31use crate::client::timeline::single_timeline::SingleTimeline;
32use crate::protocol::posting::encoded_post::EncodedPostV1;
33use crate::protocol::posting::encoded_post_bundle::EncodedPostBundleV1;
34use crate::protocol::posting::encoded_post_feedback::EncodedPostFeedbackV1;
35use crate::tools::buckets::{bucket_durations_for_type, BucketLocation, BucketType};
36use crate::tools::client_id::ClientId;
37use crate::tools::runtime_services::RuntimeServices;
38use crate::tools::types::Id;
39use bytes::Bytes;
40use log::{error, info, trace, warn};
41use std::sync::Arc;
42use tokio::sync::{RwLock, RwLockWriteGuard};
43use crate::anyhow_assert_eq;
44use crate::client::post_bundle::live_post_bundle_feedback_manager::LivePostBundleFeedbackManager;
45use crate::client::post_bundle::post_bundle_feedback_manager::PostBundleFeedbackManager;
46use crate::client::timeline::multiple_timeline::MultipleTimeline;
47use crate::protocol::payload::payload::{FetchUrlPreviewResponseV1, FetchUrlPreviewV1, PayloadRequestKind, PayloadResponseKind, PeerStatsRequestV1, PeerStatsResponseV1, SubmitPostCommitTokenV1, TrendingHashtagsFetchResponseV1, TrendingHashtagsFetchV1};
48use crate::tools::compression;
49use crate::tools::signing;
50use crate::tools::types::VerificationKey;
51use crate::protocol::peer::Peer;
52use crate::protocol::rpc;
53use crate::tools::config::CLIENT_FEEDBACK_POW_NUMERAIRE;
54use crate::tools::plain_text_post::convert_text_to_hashiverse_html;
55use crate::tools::tools;
56use crate::tools::time::TimeMillis;
57
58pub struct HashiverseClient {
84 runtime_services: Arc<RuntimeServices>,
85 client_storage: Arc<dyn ClientStorage>,
86 key_locker: Arc<dyn KeyLocker>,
87 post_bundle_manager: Arc<LivePostBundleManager>,
88 post_bundle_feedback_manager: Arc<LivePostBundleFeedbackManager>,
89 meta_post_manager: MetaPostManager,
90 client_id: ClientId,
91 peer_tracker: SharedPeerTracker,
92 recent_posts_pen: Arc<RwLock<RecentPostsPen>>,
93 single_timeline: Arc<RwLock<Option<SingleTimeline>>>,
94 multiple_timeline: Arc<RwLock<Option<MultipleTimeline>>>,
95}
96
97impl HashiverseClient {
98 pub async fn new(runtime_services: Arc<RuntimeServices>, client_storage: Arc<dyn ClientStorage>, key_locker: Arc<dyn KeyLocker>, _args: Args) -> anyhow::Result<Self> {
99 let client_id = key_locker.client_id().clone();
100 info!("client_id={}", client_id);
101
102 let peer_tracker: SharedPeerTracker = Arc::new(parking_lot::RwLock::new(PeerTracker::new(runtime_services.clone(), client_storage.clone()).await?));
103 let post_bundle_manager = Arc::new(LivePostBundleManager::new(runtime_services.clone(), client_id.id, client_storage.clone(), peer_tracker.clone()));
104 let post_bundle_feedback_manager = Arc::new(LivePostBundleFeedbackManager::new(runtime_services.clone(), client_id.id, client_storage.clone(), peer_tracker.clone()));
105
106 anyhow_assert_eq!(BUCKETS.len(), BUCKET_TRIMS.len(), "Mismatch in length between BUCKETS and BUCKET_TRIMS");
108 info!("Trimming buckets: {} buckets, {} trims", BUCKETS.len(), BUCKET_TRIMS.len());
109 for i in 0..BUCKETS.len() {
110 let trim = BUCKET_TRIMS[i];
111 if trim > 0 {
112 client_storage.trim(BUCKETS[i], trim).await?;
113 }
114 }
115
116 let meta_post_manager = MetaPostManager::new(runtime_services.clone(), client_storage.clone(), key_locker.clone(), client_id.clone());
117
118 Ok(Self {
119 runtime_services,
120 client_storage,
121 key_locker,
122 post_bundle_manager,
123 post_bundle_feedback_manager,
124 meta_post_manager,
125 client_id,
126 peer_tracker,
127 recent_posts_pen: Arc::new(RwLock::new(RecentPostsPen::new())),
128 single_timeline: Arc::new(RwLock::new(None)),
129 multiple_timeline: Arc::new(RwLock::new(None)),
130 })
131 }
132
133 pub fn client_id(&self) -> &ClientId {
134 &self.client_id
135 }
136
137 pub fn active_pow_jobs(&self) -> Vec<crate::tools::pow_generator::pow_generator::PowJobStatus> {
138 self.runtime_services.pow_generator.active_jobs()
139 }
140
141 pub fn is_pow_busy(&self, within_millis: i64) -> bool {
144 self.runtime_services.pow_generator.is_pow_busy(within_millis)
145 }
146
147 pub async fn client_storage_reset(&self) -> anyhow::Result<()> {
148 self.client_storage.reset().await
149 }
150
151 pub async fn submit_post(&self, post: &str) -> Result<(Vec<SubmitPostCommitTokenV1>, (EncodedPostV1, Bytes)), anyhow::Error> {
152 self.submit_post_with_wait(post, true).await
153 }
154
155 pub async fn submit_post_with_wait(&self, post: &str, wait_for_all_submissions: bool) -> Result<(Vec<SubmitPostCommitTokenV1>, (EncodedPostV1, Bytes)), anyhow::Error> {
159 posting::submit_post(
160 self.runtime_services.clone(),
161 &self.client_id,
162 &self.key_locker,
163 self.post_bundle_manager.clone(),
164 self.peer_tracker.clone(),
165 &self.recent_posts_pen,
166 post,
167 wait_for_all_submissions,
168 )
169 .await
170 }
171
172 pub fn meta_post_manager(&self) -> &MetaPostManager {
177 &self.meta_post_manager
178 }
179
180 pub async fn submit_meta_post(&self) -> anyhow::Result<()> {
181 let post_json = self.meta_post_manager.build_meta_post_json().await?;
182 self.submit_post(&post_json).await?;
183 Ok(())
184 }
185
186 pub async fn ensure_meta_post_in_current_bucket(&self) -> anyhow::Result<()> {
187 if self.meta_post_manager.should_auto_publish(self.post_bundle_manager.as_ref()).await? {
188 self.submit_meta_post().await?;
189 }
190 Ok(())
191 }
192
193 pub async fn submit_feedback(&self, bucket_location: BucketLocation, post_id: Id, feedback_type: u8) -> anyhow::Result<()> {
194 info!("submit_feedback: bucket_location={}, post_signature={}, feedback_type={}", bucket_location, post_id, feedback_type);
195
196 let (salt, pow, _hash) = EncodedPostFeedbackV1::pow_generate(&post_id, feedback_type, self.runtime_services.pow_generator.as_ref()).await?;
198
199 let post_bundle_location_id = bucket_location.location_id;
201 let post_bundle_feedback = self.post_bundle_feedback_manager.get_post_bundle_feedback(bucket_location.clone(), self.runtime_services.time_provider.current_time_millis()).await?;
202 let pow_best_so_far = post_bundle_feedback.get_post_pow_for_feedback_type(&post_id, feedback_type);
203 if pow <= pow_best_so_far {
204 trace!("skipping feedback submission: pow_best_so_far: {}, pow: {}", pow_best_so_far, pow);
205 return Ok(());
206 }
207
208 let encoded_post_feedback = EncodedPostFeedbackV1::new(post_id, feedback_type, salt, pow);
210 let result = posting::post_feedback_to_location(
211 &self.runtime_services, &self.client_id.id, &self.peer_tracker,
212 &bucket_location, &encoded_post_feedback,
213 ).await;
214 if let Err(e) = result {
215 warn!("Failed to feedback to {:?}: {}", post_bundle_location_id, e);
216 }
217
218 Ok(())
219 }
220
221 pub async fn get_post(&self, bucket_location: BucketLocation, post_id: &Id) -> anyhow::Result<(BucketLocation, EncodedPostV1, Bytes, bool)>
222 {
223 let time_millis = self.runtime_services.time_provider.current_time_millis();
224
225 let post_bundle = self.post_bundle_manager.get_post_bundle(&bucket_location, time_millis).await?;
227 if let Some(found_post) = Self::find_post_in_bundle(&bucket_location, &post_bundle, post_id)? {
228 return Ok(found_post);
229 }
230
231 let post_bundle = self.post_bundle_manager.get_post_bundle_settled(&bucket_location, time_millis).await?;
233 if let Some(found_post) = Self::find_post_in_bundle(&bucket_location, &post_bundle, post_id)? {
234 return Ok(found_post);
235 }
236
237 anyhow::bail!("Post {} not found in bundle {}", post_id, bucket_location.location_id)
239 }
240
241 fn find_post_in_bundle(bucket_location: &BucketLocation, post_bundle: &EncodedPostBundleV1, post_id: &Id) -> anyhow::Result<Option<(BucketLocation, EncodedPostV1, Bytes, bool)>> {
242 let mut offset = 0;
243 for i in 0..(post_bundle.header.num_posts as usize) {
244 let len = post_bundle.header.encoded_post_lengths[i];
245 if post_bundle.header.encoded_post_ids[i] == *post_id {
246 let post_bytes = post_bundle.encoded_posts_bytes.slice(offset..offset + len);
247 let encoded_post = EncodedPostV1::decode_from_bytes(post_bytes.clone(), &bucket_location.base_id, true, true)?;
248 let healed = post_bundle.header.encoded_post_healed.contains(post_id);
249 return Ok(Some((bucket_location.clone(), encoded_post, post_bytes, healed)));
250 }
251 offset += len;
252 }
253 Ok(None)
254 }
255
256 pub async fn get_post_feedbacks(&self, bucket_location: BucketLocation, post_id: Id) -> anyhow::Result<[u64; 256]>
257 {
258 let mut post_feedbacks = [0u64; 256];
259
260 let post_bundle_feedback = self.post_bundle_feedback_manager.get_post_bundle_feedback(bucket_location, self.runtime_services.time_provider.current_time_millis()).await?;
261 let post_pows = post_bundle_feedback.get_post_pows(&post_id);
262 for (i, &pow) in post_pows.iter().enumerate() {
263 if pow.0 > 0 {
264 let statistical_attempts = 1u64.checked_shl(pow.0 as u32).unwrap_or(u64::MAX);
265 post_feedbacks[i] = statistical_attempts / CLIENT_FEEDBACK_POW_NUMERAIRE as u64;
266 }
267 }
269
270 Ok(post_feedbacks)
271 }
272
273
274 async fn post_process_timeline_posts(&self, encoded_posts_bytes: Vec<(BucketLocation, Bytes, bool)>) -> anyhow::Result<Vec<(BucketLocation, EncodedPostV1, Bytes, bool)>> {
275 let mut encoded_posts = Vec::new();
276 for (bucket_location, encoded_post_bytes, healed) in encoded_posts_bytes {
277 let result = try {
278 let encoded_post = EncodedPostV1::decode_from_bytes(encoded_post_bytes.clone(), &bucket_location.base_id, true, true)?;
279 let meta_post = MetaPost::try_parse_meta_post(&encoded_post.post)?;
280 match meta_post {
281 MetaPost::None => encoded_posts.push((bucket_location, encoded_post, encoded_post_bytes, healed)),
282 MetaPost::MetaPostV1(meta_post_v1) => {
283 let post_client_id = encoded_post.header.client_id()?;
284 self.meta_post_manager.process_incoming_meta_post(&meta_post_v1, &post_client_id).await?;
285 }
286 }
287 };
288
289 if let Err(e) = result {
290 warn!("Failed to decode post: {}", e);
291 }
292 }
293
294 Ok(encoded_posts)
295 }
296
297 pub async fn single_timeline_reset(&self) -> anyhow::Result<()> {
298 info!("Resetting single timeline");
299 let mut single_timeline = self.single_timeline.write().await;
300 *single_timeline = None;
301 Ok(())
302 }
303
304 pub async fn single_timeline_lock(&self, bucket_type: BucketType, base_id: &Id) -> anyhow::Result<RwLockWriteGuard<'_, Option<SingleTimeline>>> {
305 let mut single_timeline = self.single_timeline.write().await;
306
307 if let Some(single_timeline_instance) = single_timeline.as_ref() {
309 if single_timeline_instance.bucket_type() != bucket_type || single_timeline_instance.base_id() != *base_id {
310 *single_timeline = None;
311 }
312 }
313
314 if single_timeline.is_none() {
316 trace!("Starting a new SingleTimeline for bucket_type={} base_id={}", bucket_type, base_id);
317 *single_timeline = Some(SingleTimeline::new(bucket_type, base_id, self.post_bundle_manager.clone(), self.recent_posts_pen.clone()));
318 }
319
320 Ok(single_timeline)
321 }
322
323 pub async fn single_timeline_get_more(&self, bucket_type: BucketType, base_id: &Id) -> anyhow::Result<(Vec<(BucketLocation, EncodedPostV1, Bytes, bool)>, TimeMillis)> {
324 trace!("Getting more posts for {}", base_id);
325
326 let mut single_timeline = self.single_timeline_lock(bucket_type, base_id).await?;
327 let single_timeline = single_timeline.as_mut().expect("we have ensured that our SingleTimeline exists");
328 let encoded_posts_bytes = single_timeline.get_more_posts(self.runtime_services.time_provider.current_time_millis(), 20, bucket_durations_for_type(bucket_type)).await?;
329 let oldest_processed_time_millis = single_timeline.oldest_processed_post_bundle_time_millis();
330 let posts = self.post_process_timeline_posts(encoded_posts_bytes).await?;
331 Ok((posts, oldest_processed_time_millis))
332 }
333
334 pub async fn multiple_timeline_reset(&self) -> anyhow::Result<()> {
335 info!("Resetting multiple timeline");
336 let mut multiple_timeline = self.multiple_timeline.write().await;
337 *multiple_timeline = None;
338 Ok(())
339 }
340
341 pub async fn multiple_timeline_lock(&self, bucket_type: BucketType, base_ids: &Vec<Id>) -> anyhow::Result<RwLockWriteGuard<'_, Option<MultipleTimeline>>> {
342 let mut multiple_timeline = self.multiple_timeline.write().await;
343
344 if let Some(multiple_timeline_instance) = multiple_timeline.as_ref() {
346 if multiple_timeline_instance.bucket_type() != bucket_type || multiple_timeline_instance.base_ids() != base_ids {
347 *multiple_timeline = None;
348 }
349 }
350
351 if multiple_timeline.is_none() {
353 trace!("Starting a new MultipleTimeline for base_ids.len()={}", base_ids.len());
354 *multiple_timeline = Some(MultipleTimeline::new(bucket_type, base_ids.clone(), self.post_bundle_manager.clone(), self.recent_posts_pen.clone()));
355 }
356
357 Ok(multiple_timeline)
358 }
359
360 pub async fn multiple_timeline_get_more(&self, bucket_type: BucketType, base_ids: &Vec<Id>) -> anyhow::Result<(Vec<(BucketLocation, EncodedPostV1, Bytes, bool)>, TimeMillis)> {
361 trace!("Getting more posts for base_ids.len()={}", base_ids.len());
362
363 let mut multiple_timeline = self.multiple_timeline_lock(bucket_type, base_ids).await?;
364 let multiple_timeline = multiple_timeline.as_mut().expect("we have ensured that our MultipleTimeline exists");
365 let encoded_posts_bytes = multiple_timeline.get_more_posts(self.runtime_services.time_provider.current_time_millis(), 60, 5, bucket_durations_for_type(bucket_type)).await?;
366 let oldest_processed_time_millis = multiple_timeline.oldest_processed_post_bundle_time_millis();
367 let posts = self.post_process_timeline_posts(encoded_posts_bytes).await?;
368 Ok((posts, oldest_processed_time_millis))
369 }
370
371 async fn get_random_peer(&self) -> anyhow::Result<Peer> {
372 {
373 let peer_tracker = self.peer_tracker.read();
374 if !peer_tracker.peers().is_empty() {
375 return Ok(tools::random_element(peer_tracker.peers()).clone());
376 }
377 }
378
379 PeerTracker::bootstrap(&self.peer_tracker).await?;
381 let peer_tracker = self.peer_tracker.read();
382 anyhow::ensure!(!peer_tracker.peers().is_empty(), "Still no known peers available after bootstrap");
383 Ok(tools::random_element(peer_tracker.peers()).clone())
384 }
385
386 pub async fn get_all_known_peers(&self) -> Vec<Peer> {
387 self.peer_tracker.read().peers().clone()
388 }
389
390 pub async fn fetch_url_preview(&self, url: &str) -> anyhow::Result<FetchUrlPreviewResponseV1> {
391 let peer = self.get_random_peer().await?;
392
393 let payload = FetchUrlPreviewV1::new_to_bytes(url)?;
394 let sponsor_id = self.client_id.id;
395
396 let response = rpc::rpc::rpc_server_known_with_requisite_pow(
397 &self.runtime_services,
398 &sponsor_id,
399 &peer,
400 PayloadRequestKind::FetchUrlPreviewV1,
401 payload,
402 crate::tools::config::POW_MINIMUM_PER_URL_FETCH,
403 ).await?;
404
405 anyhow::ensure!(response.response_request_kind == PayloadResponseKind::FetchUrlPreviewResponseV1, "unexpected response kind: {}", response.response_request_kind);
406 FetchUrlPreviewResponseV1::from_bytes(&response.bytes)
407 }
408
409 pub async fn fetch_peer_stats(&self, peer_id: &Id) -> anyhow::Result<serde_json::Value> {
416 let peer = {
417 let peer_tracker = self.peer_tracker.read();
418 peer_tracker.peers().iter().find(|peer| &peer.id == peer_id).cloned()
419 };
420 let peer = peer.ok_or_else(|| anyhow::anyhow!("peer not known to this client: {}", peer_id))?;
421
422 let payload = PeerStatsRequestV1 {}.to_bytes()?;
423 let sponsor_id = self.client_id.id;
424
425 let response = rpc::rpc::rpc_server_known_with_requisite_pow(
426 &self.runtime_services,
427 &sponsor_id,
428 &peer,
429 PayloadRequestKind::PeerStatsRequestV1,
430 payload,
431 crate::tools::config::POW_MINIMUM_PER_PEER_STATS,
432 ).await?;
433
434 anyhow::ensure!(response.response_request_kind == PayloadResponseKind::PeerStatsResponseV1, "unexpected response kind: {}", response.response_request_kind);
435 let response = PeerStatsResponseV1::from_bytes(&response.bytes)?;
436
437 let verification_key = VerificationKey::from_bytes(&response.peer.verification_key_bytes)?;
438 let signing_input = PeerStatsResponseV1::signing_input(response.timestamp, &response.json_compressed);
439 signing::verify(&verification_key, &response.signature, &signing_input)?;
440
441 let json_bytes = compression::decompress(&response.json_compressed)?.to_bytes();
442 let doc: serde_json::Value = serde_json::from_slice(&json_bytes)?;
443 Ok(doc)
444 }
445
446 pub async fn fetch_trending_hashtags(&self, limit: u16) -> anyhow::Result<TrendingHashtagsFetchResponseV1> {
447 let peer = self.get_random_peer().await?;
448
449 let payload = TrendingHashtagsFetchV1::new_to_bytes(limit)?;
450 let sponsor_id = self.client_id.id;
451
452 let response = rpc::rpc::rpc_server_known(
453 &self.runtime_services,
454 &sponsor_id,
455 &peer,
456 PayloadRequestKind::TrendingHashtagsFetchV1,
457 payload,
458 ).await?;
459
460 anyhow::ensure!(response.response_request_kind == PayloadResponseKind::TrendingHashtagsFetchResponseV1, "unexpected response kind: {}", response.response_request_kind);
461 TrendingHashtagsFetchResponseV1::from_bytes(&response.bytes)
462 }
463
464 pub async fn dispatch_command(&self, command: &String) -> Result<(), anyhow::Error> {
465 let mut command_parts: Vec<String> = command.splitn(2, " ").map(|s| s.to_string()).collect();
466 if command_parts.is_empty() {
467 anyhow::bail!("Command cannot be empty")
468 }
469
470 command_parts[0] = command_parts[0].to_uppercase();
471
472 match command_parts[0].as_str() {
473 "I" => {
475 info!("I am hashiverse_client {}", self.client_id);
476 }
477
478 "P" => {
480 if command_parts.len() < 2 {
481 anyhow::bail!("Post message cannot be empty")
482 }
483 let post_html = convert_text_to_hashiverse_html(&command_parts[1]);
484 let payload_response = self.submit_post(&post_html).await;
485 match payload_response {
486 Ok(_) => {
487 info!("post succeeded");
488 }
489 Err(e) => {
490 error!("post error: {}", e);
491 }
492 }
493 }
494
495 "M" => {
497 let encoded_posts = self.single_timeline_get_more(BucketType::User, &self.client_id.id).await;
498 match encoded_posts {
499 Ok((encoded_posts, _oldest_processed_time_millis)) => {
500 info!("received {} more posts", encoded_posts.len());
501 for (bucket_location_id, encoded_post, _raw_bytes, _healed) in encoded_posts {
502 info!("post: {} {} {}", bucket_location_id, encoded_post.header.time_millis, encoded_post.post);
503 }
504 }
505 Err(e) => {
506 error!("post error: {}", e);
507 }
508 }
509 }
510
511 _ => {
512 warn!("unknown command: {}", command);
513 }
514 }
515
516 Ok(())
517 }
518}