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]
25pub 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 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 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 #[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 #[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 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 #[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, }
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}