Skip to main content

hashiverse_client_wasm/
hashiverse_client_wasm.rs

1use crate::wasm_client_storage::WasmClientStorage;
2use crate::wasm_key_locker::WasmKeyLockerManager;
3use crate::wasm_transport::WasmTransportFactory;
4use crate::wasm_try;
5use hashiverse_lib::client::args::Args;
6use hashiverse_lib::client::hashiverse_client::HashiverseClient;
7use hashiverse_lib::client::key_locker::key_locker::KeyLockerManager;
8use hashiverse_lib::tools::buckets::{BucketLocation, BucketType};
9use hashiverse_lib::tools::time::TimeMillis;
10use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
11use hashiverse_lib::tools::parallel_pow_generator::{ParallelPowGenerator, StubParallelPowGenerator};
12use hashiverse_lib::tools::runtime_services::RuntimeServices;
13use hashiverse_lib::tools::types::Id;
14use log::warn;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use anyhow::anyhow;
18use tsify::Tsify;
19use wasm_bindgen::prelude::*;
20use wasm_bindgen::JsValue;
21use bytes::Bytes;
22use hashiverse_lib::protocol::posting::encoded_post::EncodedPostV1;
23
24#[wasm_bindgen]
25/// Provides a simplified dispatch interface for [HashiverseClient] to the browser.
26pub struct HashiverseClientWasm {
27    logged_in: bool,
28    hashiverse_client: HashiverseClient,
29}
30
31#[wasm_bindgen]
32impl HashiverseClientWasm {
33    async fn create_from_xxx(logged_in: bool, key_locker: Arc<dyn hashiverse_lib::client::key_locker::key_locker::KeyLocker>) -> anyhow::Result<Self> {
34        let time_provider: Arc<dyn hashiverse_lib::tools::time_provider::time_provider::TimeProvider> = Arc::new(RealTimeProvider::default());
35        let transport_factory: Arc<dyn hashiverse_lib::transport::transport::TransportFactory> = Arc::new(WasmTransportFactory::default());
36        let client_storage = WasmClientStorage::new().await?;
37        let pow_generator: Arc<dyn ParallelPowGenerator> = match crate::get_wasm_parallel_pow_generator() {
38            Some(g) => g as Arc<dyn ParallelPowGenerator>,
39            None => {
40                warn!("No native PoW generator available, falling back to StubParallelPowGenerator");
41                Arc::new(StubParallelPowGenerator::new())
42            }
43        };
44        let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
45        let hashiverse_client = HashiverseClient::new(runtime_services, client_storage, key_locker, Args::new()).await?;
46        Ok(Self { logged_in, hashiverse_client })
47    }
48
49    #[wasm_bindgen]
50    pub async fn create_from_keyphrase(key_phrase: String) -> Result<Self, JsValue> {
51        wasm_try!({
52            let logged_in = !key_phrase.is_empty();
53            let key_locker_manager = WasmKeyLockerManager::new().await?;
54            let key_locker = key_locker_manager.create(key_phrase).await?;
55            Self::create_from_xxx(logged_in, key_locker).await?
56        })
57    }
58
59    #[wasm_bindgen]
60    pub async fn create_from_stored_key(key_public: String) -> Result<Self, JsValue> {
61        wasm_try!({
62            let key_locker_manager = WasmKeyLockerManager::new().await?;
63            let key_locker = key_locker_manager.switch(key_public).await?;
64            Self::create_from_xxx(true, key_locker).await?
65        })
66    }
67
68    #[wasm_bindgen]
69    pub fn logged_in(&self) -> bool {
70        self.logged_in
71    }
72
73    #[wasm_bindgen]
74    pub async fn list_stored_key_ids_v1(&self) -> Result<Vec<String>, JsValue> {
75        wasm_try!({
76            let key_locker_manager = WasmKeyLockerManager::new().await?;
77            key_locker_manager.list().await?
78        })
79    }
80
81    #[wasm_bindgen]
82    pub async fn delete_stored_key_v1(&self, key_public: String) -> Result<(), JsValue> {
83        wasm_try!({
84            let key_locker_manager = WasmKeyLockerManager::new().await?;
85            key_locker_manager.delete(key_public).await?;
86        })
87    }
88
89    #[wasm_bindgen]
90    pub async fn delete_all_stored_keys_v1(&self) -> Result<(), JsValue> {
91        wasm_try!({
92            let key_locker_manager = WasmKeyLockerManager::new().await?;
93            key_locker_manager.reset().await?;
94        })
95    }
96
97    #[wasm_bindgen]
98    pub fn get_client_id(&self) -> String {
99        self.hashiverse_client.client_id().id_hex()
100    }
101
102    #[wasm_bindgen]
103    pub async fn client_storage_reset(&self) -> Result<(), JsValue> {
104        wasm_try!({
105            self.hashiverse_client.client_storage_reset().await?;
106        })
107    }
108
109    #[wasm_bindgen]
110    pub async fn post_v1(&self, post: &str) -> Result<Post, JsValue> {
111        wasm_try!({
112            let (commit_tokens, (encoded_post, raw_bytes)) = self.hashiverse_client.submit_post(post).await?;
113            let bucket_location = &commit_tokens[0].bucket_location;
114            let client_id = encoded_post.header.client_id()?;
115            let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
116            Post {
117                post_id: encoded_post.post_id.to_hex_str(),
118                time_millis: encoded_post.header.time_millis.0,
119                client_id: client_id.id_hex(),
120                bucket_location: bucket_location.to_html_attr(),
121                post: encoded_post.post,
122                encoded_post_header_hex,
123            }
124        })
125    }
126
127    fn meta_post_manager(&self) -> &hashiverse_lib::client::meta_post::meta_post_manager::MetaPostManager {
128        self.hashiverse_client.meta_post_manager()
129    }
130
131    pub async fn set_bio(&self, nickname: String, status: String, selfie: String, avatar: String) -> Result<(), JsValue> {
132        wasm_try!({
133            self.meta_post_manager().set_bio(nickname, status, selfie, avatar).await?;
134        })
135    }
136
137    #[wasm_bindgen]
138    pub async fn submit_feedback_v1(&self, bucket_location: String, post_id: String, feedback_type: u8) -> Result<(), JsValue> {
139        wasm_try!({
140            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
141            let post_id = Id::from_hex_str(&post_id)?;
142            self.hashiverse_client.submit_feedback(bucket_location, post_id, feedback_type).await?;
143        })
144    }
145
146    #[wasm_bindgen]
147    /// Gets a specific post
148    pub async fn get_post_v1(&self, bucket_location: String, post_id: String) -> Result<Post, JsValue> {
149        wasm_try!({
150            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
151            let post_id = Id::from_hex_str(&post_id)?;
152            let (bucket_location, post, raw_bytes) = self.hashiverse_client.get_post(bucket_location, &post_id).await?;
153            let client_id = post.header.client_id()?;
154            let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
155            Post {
156                post_id: post.post_id.to_hex_str(),
157                time_millis: post.header.time_millis.0,
158                client_id: client_id.id_hex(),
159                bucket_location: bucket_location.to_html_attr(),
160                post: post.post,
161                encoded_post_header_hex,
162            }
163        })
164    }
165
166    #[wasm_bindgen]
167    /// Gets all the feedbacks for a specific post
168    ///
169    /// The resulting vector has 256 entries - one per feedback_type that have been mapped to the statistical number of clicks a feedback button has received.
170    pub async fn get_post_feedbacks_v1(&self, bucket_location: String, post_id: String) -> Result<Vec<u32>, JsValue> {
171        wasm_try!({
172            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
173            let post_id = Id::from_hex_str(&post_id)?;
174            let post_feedbacks = self.hashiverse_client.get_post_feedbacks(bucket_location, post_id).await?;
175            post_feedbacks.iter().map(|&feedback| feedback.min(u32::MAX as u64) as u32).collect()
176        })
177    }
178
179    #[wasm_bindgen]
180    pub async fn get_bio(&self, id: String) -> Result<Bio, JsValue> {
181        wasm_try!({
182            let meta_post_public = self.meta_post_manager().get_meta_post_public(Id::from_hex_str(&id)?).await?;
183            match meta_post_public {
184                Some(meta_post_public) => Bio {
185                    client_id: id,
186                    nickname: meta_post_public.nickname.value.unwrap_or_default(),
187                    status: meta_post_public.status.value.unwrap_or_default(),
188                    selfie: meta_post_public.selfie.value.unwrap_or_default(),
189                    avatar: meta_post_public.avatar.value.unwrap_or_default(),
190                },
191                None => Bio {
192                    client_id: id,
193                    nickname: "".to_string(),
194                    status: "".to_string(),
195                    selfie: "".to_string(),
196                    avatar: "".to_string(),
197                },
198            }
199        })
200    }
201
202    #[wasm_bindgen]
203    pub async fn get_all_bios(&self) -> Result<Vec<Bio>, JsValue> {
204        wasm_try!({
205            let meta_post_publics = self.meta_post_manager().get_all_meta_post_publics().await?;
206            meta_post_publics.into_iter()
207                .map(|(client_id, meta_post_public)| Bio {
208                    client_id,
209                    nickname: meta_post_public.nickname.value.unwrap_or_default(),
210                    status: meta_post_public.status.value.unwrap_or_default(),
211                    selfie: meta_post_public.selfie.value.unwrap_or_default(),
212                    avatar: meta_post_public.avatar.value.unwrap_or_default(),
213                })
214                .collect()
215        })
216    }
217
218    fn post_process_timeline_posts(&self, encoded_posts: Vec<(BucketLocation, EncodedPostV1, Bytes)>, oldest_processed_time_millis: TimeMillis) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
219        let response = SingleTimelineGetMoreV1Response {
220            oldest_processed_time_millis: if oldest_processed_time_millis == TimeMillis::MAX { None } else { Some(oldest_processed_time_millis.0) },
221            posts: encoded_posts
222                .into_iter()
223                .filter_map(|(bucket_location, post, raw_bytes)| {
224                    let client_id = match post.header.client_id() {
225                        Ok(client_id) => client_id,
226                        Err(e) => {
227                            warn!("Skipping post with bad client_id in header: {}", e);
228                            return None;
229                        }
230                    };
231                    let encoded_post_header_hex = match EncodedPostV1::bytes_without_body(raw_bytes) {
232                        Ok(header_bytes) => hex::encode(header_bytes),
233                        Err(e) => {
234                            warn!("Skipping post with bad header bytes: {}", e);
235                            return None;
236                        }
237                    };
238                    Some(Post {
239                        post_id: post.post_id.to_hex_str(),
240                        time_millis: post.header.time_millis.0,
241                        client_id: client_id.id_hex(),
242                        bucket_location: bucket_location.to_html_attr(),
243                        post: post.post,
244                        encoded_post_header_hex,
245                    })
246                })
247                .collect(),
248        };
249
250        Ok(response)
251    }
252
253
254    #[wasm_bindgen]
255    pub async fn single_timeline_reset(&self) -> Result<(), JsValue> {
256        wasm_try!({
257            self.hashiverse_client.single_timeline_reset().await?;
258        })
259    }
260
261    async fn single_timeline_get_more(&self, bucket_type: BucketType, base_id: &Id) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
262        let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.single_timeline_get_more(bucket_type, base_id).await?;
263        self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
264    }
265
266    #[wasm_bindgen]
267    pub async fn single_timeline_get_more_me_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
268        wasm_try!({
269            let id = self.hashiverse_client.client_id().id;
270            self.single_timeline_get_more(BucketType::User, &id).await?
271        })
272    }
273
274    #[wasm_bindgen]
275    pub async fn single_timeline_get_more_me_mentioned_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
276        wasm_try!({
277            let id = self.hashiverse_client.client_id().id;
278            self.single_timeline_get_more(BucketType::Mention, &id).await?
279        })
280    }
281
282    #[wasm_bindgen]
283    pub async fn single_timeline_get_more_hashtag_v1(&self, hashtag: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
284        wasm_try!({
285            let id = Id::from_hashtag_str(&hashtag)?;
286            self.single_timeline_get_more(BucketType::Hashtag, &id).await?
287        })
288    }
289
290    #[wasm_bindgen]
291    pub async fn single_timeline_get_more_user_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
292        wasm_try!({
293            let id = Id::from_hex_str(&client_id_hex)?;
294            self.single_timeline_get_more(BucketType::User, &id).await?
295        })
296    }
297
298    #[wasm_bindgen]
299    pub async fn single_timeline_get_more_user_mentioned_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
300        wasm_try!({
301            let id = Id::from_hex_str(&client_id_hex)?;
302            self.single_timeline_get_more(BucketType::Mention, &id).await?
303        })
304    }
305
306    #[wasm_bindgen]
307    pub async fn single_timeline_get_more_reply_to_post_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
308        wasm_try!({
309            let id = Id::from_hex_str(&post_id)?;
310            self.single_timeline_get_more(BucketType::ReplyToPost, &id).await?
311        })
312    }
313
314    #[wasm_bindgen]
315    pub async fn single_timeline_get_more_sequel_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
316        wasm_try!({
317            let id = Id::from_hex_str(&post_id)?;
318            self.single_timeline_get_more(BucketType::Sequel, &id).await?
319        })
320    }
321
322    #[wasm_bindgen]
323    pub async fn multiple_timeline_reset(&self) -> Result<(), JsValue> {
324        wasm_try!({
325            self.hashiverse_client.multiple_timeline_reset().await?;
326        })
327    }
328
329    async fn multiple_timeline_get_more(&self, bucket_type: BucketType, base_ids: &Vec<Id>) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
330        let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.multiple_timeline_get_more(bucket_type, base_ids).await?;
331        self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
332    }
333
334    #[wasm_bindgen]
335    pub async fn multiple_timeline_get_more_followed_users(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
336        wasm_try!({
337            let ids = self.meta_post_manager().get_followed_client_ids().await?;
338            self.multiple_timeline_get_more(BucketType::User, &ids).await?
339        })
340    }
341
342    #[wasm_bindgen]
343    pub async fn get_followed_client_ids_v1(&self) -> Result<Vec<String>, JsValue> {
344        wasm_try!({
345            let ids = self.meta_post_manager().get_followed_client_ids().await?;
346            ids.into_iter().map(|id| id.to_hex_str()).collect()
347        })
348    }
349
350    #[wasm_bindgen]
351    pub async fn set_followed_client_ids_v1(&self, client_ids: JsValue) -> Result<(), JsValue> {
352        wasm_try!({
353            let client_id_strs: Vec<String> = serde_wasm_bindgen::from_value(client_ids).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
354            let ids = client_id_strs.iter().map(|s| Id::from_hex_str(s)).collect::<anyhow::Result<Vec<_>>>()?;
355            self.meta_post_manager().set_followed_client_ids(ids).await?;
356        })
357    }
358
359    #[wasm_bindgen]
360    pub async fn set_followed_client_id_v1(&self, client_id: String, is_followed: bool) -> Result<(), JsValue> {
361        wasm_try!({
362            let id = Id::from_hex_str(&client_id)?;
363            self.meta_post_manager().set_followed_client_id(id, is_followed).await?;
364        })
365    }
366
367    #[wasm_bindgen]
368    pub async fn multiple_timeline_get_more_followed_hashtags(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
369        wasm_try!({
370            let hashtags = self.meta_post_manager().get_followed_hashtags().await?;
371            let ids = hashtags.iter().map(|h| Id::from_hashtag_str(h)).collect::<anyhow::Result<Vec<_>>>()?;
372            self.multiple_timeline_get_more(BucketType::Hashtag, &ids).await?
373        })
374    }
375
376    #[wasm_bindgen]
377    pub async fn get_followed_hashtags_v1(&self) -> Result<Vec<String>, JsValue> {
378        wasm_try!({
379            self.meta_post_manager().get_followed_hashtags().await?
380        })
381    }
382
383    #[wasm_bindgen]
384    pub async fn set_followed_hashtags_v1(&self, hashtags: JsValue) -> Result<(), JsValue> {
385        wasm_try!({
386            let hashtags: Vec<String> = serde_wasm_bindgen::from_value(hashtags).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
387            self.meta_post_manager().set_followed_hashtags(hashtags).await?;
388        })
389    }
390
391    #[wasm_bindgen]
392    pub async fn set_followed_hashtag_v1(&self, hashtag: String, is_followed: bool) -> Result<(), JsValue> {
393        wasm_try!({
394            self.meta_post_manager().set_followed_hashtag(hashtag, is_followed).await?;
395        })
396    }
397
398    // ------------------------------------------------------------------
399    // MetaPostV1 — unified config publish
400    // ------------------------------------------------------------------
401
402    #[wasm_bindgen]
403    pub async fn submit_meta_post_v1(&self) -> Result<(), JsValue> {
404        wasm_try!({
405            self.hashiverse_client.submit_meta_post().await?;
406        })
407    }
408
409    #[wasm_bindgen]
410    pub async fn ensure_meta_post_in_current_bucket_v1(&self) -> Result<(), JsValue> {
411        wasm_try!({
412            self.hashiverse_client.ensure_meta_post_in_current_bucket().await?;
413        })
414    }
415
416    // ------------------------------------------------------------------
417    // Content thresholds
418    // ------------------------------------------------------------------
419
420    #[wasm_bindgen]
421    pub async fn get_content_thresholds_v1(&self) -> Result<JsValue, JsValue> {
422        wasm_try!({
423            let thresholds = self.meta_post_manager().get_content_thresholds().await?;
424            // serde_wasm_bindgen serializes HashMaps as JS Map (not plain object) and rejects
425            // non-string keys with serialize_maps_as_objects.  Convert to String keys so the
426            // result is a plain JS object matching TS Record<number, number>.
427            let thresholds_js: std::collections::HashMap<String, u32> = thresholds.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
428            let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
429            thresholds_js.serialize(&serializer).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?
430        })
431    }
432
433    #[wasm_bindgen]
434    pub async fn set_content_thresholds_v1(&self, thresholds: JsValue) -> Result<(), JsValue> {
435        wasm_try!({
436            let thresholds_str: std::collections::HashMap<String, u32> = serde_wasm_bindgen::from_value(thresholds).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?;
437            let thresholds: std::collections::HashMap<u8, u32> = thresholds_str.into_iter()
438                .map(|(k, v)| Ok((k.parse::<u8>().map_err(|e| anyhow!("invalid feedback_type key: {}", e))?, v)))
439                .collect::<anyhow::Result<_>>()?;
440            self.meta_post_manager().set_content_thresholds(thresholds).await?;
441        })
442    }
443
444    // ------------------------------------------------------------------
445    // Skip warnings for followed
446    // ------------------------------------------------------------------
447
448    #[wasm_bindgen]
449    pub async fn get_skip_warnings_for_followed_v1(&self) -> Result<bool, JsValue> {
450        wasm_try!({
451            self.meta_post_manager().get_skip_warnings_for_followed().await?
452        })
453    }
454
455    #[wasm_bindgen]
456    pub async fn set_skip_warnings_for_followed_v1(&self, value: bool) -> Result<(), JsValue> {
457        wasm_try!({
458            self.meta_post_manager().set_skip_warnings_for_followed(value).await?;
459        })
460    }
461
462    #[wasm_bindgen]
463    pub async fn fetch_url_preview_v1(&self, url: String) -> Result<UrlPreview, JsValue> {
464        wasm_try!({
465            let preview = self.hashiverse_client.fetch_url_preview(&url).await?;
466            UrlPreview {
467                url: preview.url,
468                title: preview.title,
469                description: preview.description,
470                image_url: preview.image_url,
471            }
472        })
473    }
474
475    #[wasm_bindgen]
476    pub async fn fetch_trending_hashtags_v1(&self, limit: u16) -> Result<TrendingHashtagsFetchResponse, JsValue> {
477        wasm_try!({
478            let response = self.hashiverse_client.fetch_trending_hashtags(limit).await?;
479            TrendingHashtagsFetchResponse {
480                trending_hashtags: response.trending_hashtags.into_iter().map(|entry| TrendingHashtag {
481                    hashtag: entry.hashtag,
482                    count: entry.count,
483                }).collect(),
484            }
485        })
486    }
487}
488
489#[derive(Tsify, Serialize, Deserialize)]
490#[tsify(into_wasm_abi)]
491pub struct SingleTimelineGetMoreV1Response {
492    pub posts: Vec<Post>,
493    pub oldest_processed_time_millis: Option<i64>,
494}
495
496#[derive(Tsify, Serialize, Deserialize)]
497#[tsify(into_wasm_abi)]
498pub struct Post {
499    pub post_id: String,
500    pub time_millis: i64,
501    pub client_id: String,
502    pub bucket_location: String,
503    pub post: String,
504    pub encoded_post_header_hex: String, // contains the hex-encoded EncodedPost without the post body
505}
506
507#[derive(Tsify, Serialize, Deserialize)]
508#[tsify(into_wasm_abi)]
509pub struct Bio {
510    pub client_id: String,
511    pub nickname: String,
512    pub status: String,
513    pub selfie: String,
514    pub avatar: String,
515}
516
517#[derive(Tsify, Serialize, Deserialize)]
518#[tsify(into_wasm_abi)]
519pub struct UrlPreview {
520    pub url: String,
521    pub title: String,
522    pub description: String,
523    pub image_url: String,
524}
525
526#[derive(Tsify, Serialize, Deserialize)]
527#[tsify(into_wasm_abi)]
528pub struct TrendingHashtag {
529    pub hashtag: String,
530    pub count: u64,
531}
532
533#[derive(Tsify, Serialize, Deserialize)]
534#[tsify(into_wasm_abi)]
535pub struct TrendingHashtagsFetchResponse {
536    pub trending_hashtags: Vec<TrendingHashtag>,
537}