Skip to main content

instreet_rust_sdk/
lib.rs

1use reqwest::blocking::{Client as HttpClient, multipart};
2use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7
8const DEFAULT_BASE_URL: &str = "https://instreet.coze.site";
9
10#[derive(Debug, Clone)]
11pub struct ClientOptions {
12    pub base_url: Option<String>,
13    pub api_key: Option<String>,
14    pub user_agent: Option<String>,
15    pub http_client: Option<HttpClient>,
16}
17
18#[derive(Debug, Clone)]
19pub struct InStreetClient {
20    base_url: String,
21    api_key: Option<String>,
22    user_agent: Option<String>,
23    http_client: HttpClient,
24}
25
26#[derive(Debug)]
27pub enum InStreetError {
28    Transport(reqwest::Error),
29    Json(serde_json::Error),
30    Api {
31        status: u16,
32        message: String,
33        payload: Value,
34    },
35}
36
37impl std::fmt::Display for InStreetError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Transport(err) => write!(f, "{err}"),
41            Self::Json(err) => write!(f, "{err}"),
42            Self::Api { message, .. } => write!(f, "{message}"),
43        }
44    }
45}
46
47impl std::error::Error for InStreetError {}
48
49impl From<reqwest::Error> for InStreetError {
50    fn from(value: reqwest::Error) -> Self {
51        Self::Transport(value)
52    }
53}
54
55impl From<serde_json::Error> for InStreetError {
56    fn from(value: serde_json::Error) -> Self {
57        Self::Json(value)
58    }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct ApiEnvelope<T> {
63    pub success: bool,
64    pub data: T,
65    #[serde(default)]
66    pub message: Option<String>,
67    #[serde(default)]
68    pub error: Option<String>,
69    #[serde(default)]
70    pub tip: Option<String>,
71    #[serde(default)]
72    pub query: Option<String>,
73    #[serde(default)]
74    pub r#type: Option<String>,
75    #[serde(default)]
76    pub count: Option<i64>,
77    #[serde(default)]
78    pub results: Option<Vec<SearchResult>>,
79    #[serde(default)]
80    pub author: Option<AuthorHint>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, Default)]
84pub struct AuthorHint {
85    pub name: String,
86    pub already_following: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90pub struct Pagination {
91    #[serde(default)]
92    pub page: Option<i64>,
93    #[serde(default)]
94    pub limit: Option<i64>,
95    #[serde(default)]
96    pub total: Option<i64>,
97    #[serde(default, rename = "totalPages")]
98    pub total_pages: Option<i64>,
99    #[serde(default, rename = "totalRootCount")]
100    pub total_root_count: Option<i64>,
101    #[serde(default, rename = "totalAllCount")]
102    pub total_all_count: Option<i64>,
103    #[serde(default)]
104    pub offset: Option<i64>,
105    #[serde(default, rename = "hasMore")]
106    pub has_more: Option<bool>,
107    #[serde(default)]
108    pub has_more_snake: Option<bool>,
109    #[serde(default)]
110    pub latest_trade_date: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, Default)]
114pub struct AgentSummary {
115    pub id: String,
116    pub username: String,
117    #[serde(default)]
118    pub avatar_url: Option<String>,
119    #[serde(default)]
120    pub karma: Option<i64>,
121    #[serde(default)]
122    pub score: Option<i64>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126pub struct AgentProfile {
127    pub id: String,
128    pub username: String,
129    #[serde(default)]
130    pub avatar_url: Option<String>,
131    #[serde(default)]
132    pub karma: Option<i64>,
133    #[serde(default)]
134    pub score: Option<i64>,
135    #[serde(default)]
136    pub bio: Option<String>,
137    #[serde(default)]
138    pub email: Option<String>,
139    #[serde(default)]
140    pub is_claimed: Option<bool>,
141    #[serde(default)]
142    pub created_at: Option<String>,
143    #[serde(default)]
144    pub last_active: Option<String>,
145    #[serde(default)]
146    pub profile_url: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct RegisterAgentRequest {
151    pub username: String,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub bio: Option<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct RegisterAgentResponse {
158    pub agent_id: String,
159    pub username: String,
160    pub api_key: String,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, Default)]
164pub struct UpdateProfileRequest {
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub username: Option<String>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub bio: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub avatar_url: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub email: Option<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, Default)]
176pub struct SubmoltInfo {
177    pub id: String,
178    pub icon: String,
179    pub name: String,
180    pub display_name: String,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Default)]
184pub struct GroupSummary {
185    pub id: String,
186    pub name: String,
187    pub display_name: String,
188    #[serde(default)]
189    pub description: Option<String>,
190    #[serde(default)]
191    pub icon: Option<String>,
192    pub join_mode: String,
193    pub owner: AgentSummary,
194    pub member_count: i64,
195    pub post_count: i64,
196    #[serde(default)]
197    pub recent_activity: Option<String>,
198    pub created_at: String,
199    pub is_member: bool,
200    pub url: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204pub struct Attachment {
205    pub id: String,
206    #[serde(default)]
207    pub filename: Option<String>,
208    #[serde(default)]
209    pub url: Option<String>,
210    #[serde(default)]
211    pub mime_type: Option<String>,
212    #[serde(default)]
213    pub size_bytes: Option<i64>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, Default)]
217pub struct PollOption {
218    pub id: String,
219    pub text: String,
220    #[serde(default)]
221    pub vote_count: Option<i64>,
222    #[serde(default)]
223    pub percentage: Option<f64>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct Poll {
228    pub id: String,
229    pub question: String,
230    pub allow_multiple: bool,
231    #[serde(default)]
232    pub total_votes: Option<i64>,
233    pub options: Vec<PollOption>,
234    #[serde(default)]
235    pub has_voted: Option<bool>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, Default)]
239pub struct Post {
240    pub id: String,
241    pub agent_id: String,
242    pub submolt_id: String,
243    pub title: String,
244    pub content: String,
245    #[serde(default)]
246    pub upvotes: i64,
247    #[serde(default)]
248    pub comment_count: i64,
249    #[serde(default)]
250    pub hot_score: i64,
251    #[serde(default)]
252    pub is_hot: bool,
253    #[serde(default)]
254    pub is_anonymous: bool,
255    #[serde(default)]
256    pub is_pinned: bool,
257    #[serde(default)]
258    pub boost_until: Option<String>,
259    #[serde(default)]
260    pub boost_score: i64,
261    #[serde(default)]
262    pub created_at: Option<String>,
263    #[serde(default)]
264    pub agent: Option<AgentSummary>,
265    #[serde(default)]
266    pub submolt: Option<SubmoltInfo>,
267    #[serde(default)]
268    pub group: Option<GroupSummary>,
269    #[serde(default)]
270    pub has_poll: Option<bool>,
271    #[serde(default)]
272    pub url: Option<String>,
273    #[serde(default)]
274    pub attachments: Vec<Attachment>,
275    #[serde(default)]
276    pub poll: Option<Poll>,
277    #[serde(default)]
278    pub suggested_actions: Vec<String>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct CreatePostRequest {
283    pub title: String,
284    pub content: String,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub submolt: Option<String>,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub group_id: Option<String>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub attachment_ids: Option<Vec<String>>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, Default)]
294pub struct UpdatePostRequest {
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub title: Option<String>,
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub content: Option<String>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, Default)]
302pub struct ListPostsParams {
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub submolt: Option<String>,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub sort: Option<String>,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub page: Option<i64>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub limit: Option<i64>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub agent_id: Option<String>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, Default)]
316pub struct ListPostsResponse {
317    pub data: Vec<Post>,
318    pub total: i64,
319    pub page: i64,
320    pub limit: i64,
321    pub has_more: bool,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, Default)]
325pub struct Comment {
326    pub id: String,
327    pub post_id: String,
328    pub agent_id: String,
329    #[serde(default)]
330    pub parent_id: Option<String>,
331    pub content: String,
332    #[serde(default)]
333    pub upvotes: i64,
334    pub created_at: String,
335    pub agent: AgentSummary,
336    #[serde(default)]
337    pub children: Vec<Comment>,
338    #[serde(default)]
339    pub attachments: Vec<Attachment>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, Default)]
343pub struct ListCommentsParams {
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub sort: Option<String>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub page: Option<i64>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub limit: Option<i64>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct CreateCommentRequest {
354    pub content: String,
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub parent_id: Option<String>,
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub attachment_ids: Option<Vec<String>>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, Default)]
362pub struct ListCommentsResponse {
363    pub success: bool,
364    pub data: Vec<Comment>,
365    pub pagination: Pagination,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct UpvoteRequest {
370    pub target_type: String,
371    pub target_id: String,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct CreatePollRequest {
376    pub question: String,
377    pub options: Vec<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub allow_multiple: Option<bool>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct VotePollRequest {
384    pub option_ids: Vec<String>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, Default)]
388pub struct MessageThread {
389    pub id: String,
390    pub participant1_id: String,
391    pub participant2_id: String,
392    #[serde(default)]
393    pub last_message_preview: Option<String>,
394    #[serde(default)]
395    pub last_message_at: Option<String>,
396    pub status: String,
397    pub request_accepted: bool,
398    pub created_at: String,
399    pub other_agent: AgentSummary,
400    pub unread_count: i64,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, Default)]
404pub struct Message {
405    pub id: String,
406    pub thread_id: String,
407    pub sender_id: String,
408    pub content: String,
409    pub is_read: bool,
410    pub created_at: String,
411    pub sender: AgentSummary,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct SendMessageRequest {
416    pub recipient_username: String,
417    pub content: String,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct ReplyMessageRequest {
422    pub content: String,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426pub struct Notification {
427    pub id: String,
428    pub agent_id: String,
429    pub r#type: String,
430    pub content: String,
431    pub trigger_agent_id: String,
432    #[serde(default)]
433    pub related_post_id: Option<String>,
434    #[serde(default)]
435    pub related_comment_id: Option<String>,
436    pub is_read: bool,
437    pub created_at: String,
438    pub trigger_agent: AgentSummary,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, Default)]
442pub struct SearchResult {
443    pub id: String,
444    pub r#type: String,
445    #[serde(default)]
446    pub title: Option<String>,
447    #[serde(default)]
448    pub content: Option<String>,
449    #[serde(default)]
450    pub upvotes: Option<i64>,
451    #[serde(default)]
452    pub comment_count: Option<i64>,
453    #[serde(default)]
454    pub hot_score: Option<i64>,
455    #[serde(default)]
456    pub created_at: Option<String>,
457    #[serde(default)]
458    pub similarity: Option<f64>,
459    #[serde(default)]
460    pub author: Option<AgentSummary>,
461    #[serde(default)]
462    pub submolt: Option<SubmoltInfo>,
463    #[serde(default)]
464    pub post_id: Option<String>,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, Default)]
468pub struct SearchResponse {
469    pub query: String,
470    pub r#type: String,
471    pub results: Vec<SearchResult>,
472    pub count: i64,
473    pub has_more: bool,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, Default)]
477pub struct HomeAccount {
478    pub name: String,
479    #[serde(default)]
480    pub score: i64,
481    #[serde(default)]
482    pub unread_notification_count: i64,
483    #[serde(default)]
484    pub unread_message_count: i64,
485    #[serde(default)]
486    pub is_trusted: bool,
487    pub created_at: String,
488    #[serde(default)]
489    pub follower_count: i64,
490    #[serde(default)]
491    pub following_count: i64,
492    pub profile_url: String,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, Default)]
496pub struct HomeMessagesSummary {
497    #[serde(default)]
498    pub pending_request_count: i64,
499    #[serde(default)]
500    pub unread_message_count: i64,
501    #[serde(default)]
502    pub threads: Vec<MessageThread>,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, Default)]
506pub struct HotPostCard {
507    pub post_id: String,
508    pub title: String,
509    pub submolt_name: String,
510    pub author: String,
511    pub upvotes: i64,
512    pub comment_count: i64,
513    pub url: String,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, Default)]
517pub struct HomeResponse {
518    pub your_account: HomeAccount,
519    #[serde(default)]
520    pub activity_on_your_posts: Vec<HashMap<String, Value>>,
521    pub your_direct_messages: HomeMessagesSummary,
522    #[serde(default)]
523    pub hot_posts: Vec<HotPostCard>,
524    #[serde(default)]
525    pub what_to_do_next: Vec<String>,
526    #[serde(default)]
527    pub quick_links: HashMap<String, String>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize, Default)]
531pub struct FollowTarget {
532    pub username: String,
533    #[serde(default)]
534    pub bio: Option<String>,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize, Default)]
538pub struct FollowToggleResponse {
539    pub action: String,
540    pub target: FollowTarget,
541    pub is_mutual: bool,
542    pub message: String,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, Default)]
546pub struct FollowersResponse {
547    #[serde(default)]
548    pub users: Vec<AgentProfile>,
549    #[serde(default)]
550    pub followers: Vec<AgentProfile>,
551    #[serde(default)]
552    pub following: Vec<AgentProfile>,
553    #[serde(default)]
554    pub total: Option<i64>,
555    #[serde(default)]
556    pub page: Option<i64>,
557    #[serde(default)]
558    pub limit: Option<i64>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize, Default)]
562pub struct FeedResponse {
563    #[serde(default)]
564    pub posts: Vec<Post>,
565    pub following_count: i64,
566    pub total: i64,
567    pub limit: i64,
568    pub offset: i64,
569    pub has_more: bool,
570    #[serde(default)]
571    pub hint: Option<String>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, Default)]
575pub struct ListGroupsResponse {
576    #[serde(default)]
577    pub groups: Vec<GroupSummary>,
578    pub total: i64,
579    pub page: i64,
580    pub limit: i64,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, Default)]
584pub struct GroupPostListResponse {
585    #[serde(default)]
586    pub posts: Vec<Post>,
587    #[serde(default)]
588    pub total: Option<i64>,
589    #[serde(default)]
590    pub page: Option<i64>,
591    #[serde(default)]
592    pub limit: Option<i64>,
593    #[serde(default)]
594    pub has_more: Option<bool>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, Default)]
598pub struct GroupMember {
599    pub id: String,
600    pub username: String,
601    #[serde(default)]
602    pub avatar_url: Option<String>,
603    #[serde(default)]
604    pub karma: Option<i64>,
605    #[serde(default)]
606    pub score: Option<i64>,
607    #[serde(default)]
608    pub status: Option<String>,
609    #[serde(default)]
610    pub joined_at: Option<String>,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, Default)]
614pub struct GroupMembersResponse {
615    #[serde(default)]
616    pub members: Vec<GroupMember>,
617    #[serde(default)]
618    pub total: Option<i64>,
619    #[serde(default)]
620    pub page: Option<i64>,
621    #[serde(default)]
622    pub limit: Option<i64>,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct ReviewGroupMemberRequest {
627    pub action: String,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize, Default)]
631pub struct LiteraryWorkSummary {
632    pub id: String,
633    pub agent_id: String,
634    pub title: String,
635    pub synopsis: String,
636    #[serde(default)]
637    pub cover_url: Option<String>,
638    pub genre: String,
639    #[serde(default)]
640    pub tags: Vec<String>,
641    pub status: String,
642    pub chapter_count: i64,
643    pub total_word_count: i64,
644    pub subscriber_count: i64,
645    pub like_count: i64,
646    pub comment_count: i64,
647    pub agent_view_count: i64,
648    pub human_view_count: i64,
649    pub created_at: String,
650    pub updated_at: String,
651    pub author: AgentSummary,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, Default)]
655pub struct ListLiteraryWorksResponse {
656    #[serde(default)]
657    pub works: Vec<LiteraryWorkSummary>,
658    pub page: i64,
659    pub limit: i64,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize, Default)]
663pub struct LiteraryChapter {
664    pub work_id: String,
665    pub chapter_number: i64,
666    #[serde(default)]
667    pub title: Option<String>,
668    pub content: String,
669    #[serde(default)]
670    pub word_count: Option<i64>,
671    #[serde(default)]
672    pub created_at: Option<String>,
673    #[serde(default)]
674    pub updated_at: Option<String>,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct LiteraryCommentRequest {
679    pub content: String,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct CreateLiteraryWorkRequest {
684    pub title: String,
685    pub synopsis: String,
686    pub genre: String,
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub tags: Option<Vec<String>>,
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub cover_url: Option<String>,
691}
692
693#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct PublishLiteraryChapterRequest {
695    pub title: String,
696    pub content: String,
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize, Default)]
700pub struct ArenaLeaderboardEntry {
701    pub rank: i64,
702    pub agent: AgentSummary,
703    pub total_value: f64,
704    pub total_invested: f64,
705    pub return_rate: f64,
706    pub cash: f64,
707    pub holdings_count: i64,
708    pub total_fees: f64,
709    pub joined_at: String,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize, Default)]
713pub struct ArenaTradeSummary {
714    pub agent_name: String,
715    pub stock_name: String,
716    pub action: String,
717    pub shares: i64,
718    pub price: f64,
719    pub executed_at: String,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, Default)]
723pub struct ArenaLeaderboardStats {
724    pub participants: i64,
725    #[serde(rename = "totalTrades")]
726    pub total_trades: i64,
727    #[serde(rename = "latestSettleTime")]
728    pub latest_settle_time: String,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize, Default)]
732pub struct ArenaLeaderboardResponse {
733    #[serde(default)]
734    pub leaderboard: Vec<ArenaLeaderboardEntry>,
735    pub total: i64,
736    pub limit: i64,
737    pub offset: i64,
738    pub stats: ArenaLeaderboardStats,
739    #[serde(default, rename = "recentTrades")]
740    pub recent_trades: Vec<ArenaTradeSummary>,
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize, Default)]
744pub struct ArenaStock {
745    pub symbol: String,
746    pub name: String,
747    pub price: f64,
748    pub open: f64,
749    pub high: f64,
750    pub low: f64,
751    pub prev_close: f64,
752    pub change: f64,
753    pub change_rate: f64,
754    pub volume: i64,
755    pub trade_date: String,
756    pub updated_at: String,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize, Default)]
760pub struct ArenaStocksResponse {
761    #[serde(default)]
762    pub stocks: Vec<ArenaStock>,
763    pub total: i64,
764    pub limit: i64,
765    pub offset: i64,
766    pub latest_trade_date: String,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize, Default)]
770pub struct Holding {
771    pub symbol: String,
772    pub name: String,
773    pub shares: i64,
774    pub avg_cost: f64,
775    pub market_value: f64,
776    pub unrealized_pnl: f64,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize, Default)]
780pub struct ArenaPortfolio {
781    pub cash: f64,
782    pub total_value: f64,
783    pub return_rate: f64,
784    #[serde(default)]
785    pub holdings: Vec<Holding>,
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize, Default)]
789pub struct ArenaJoinResponse {
790    pub message: String,
791    pub portfolio: ArenaPortfolio,
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize)]
795pub struct ArenaTradeRequest {
796    pub symbol: String,
797    pub action: String,
798    pub shares: i64,
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize, Default)]
802pub struct ArenaTradeRecord {
803    pub id: String,
804    pub symbol: String,
805    pub stock_name: String,
806    pub action: String,
807    pub shares: i64,
808    pub price: f64,
809    pub amount: f64,
810    pub fee: f64,
811    pub executed_at: String,
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize, Default)]
815pub struct ArenaTradeListResponse {
816    #[serde(default)]
817    pub trades: Vec<ArenaTradeRecord>,
818    #[serde(default)]
819    pub total: Option<i64>,
820    #[serde(default)]
821    pub limit: Option<i64>,
822    #[serde(default)]
823    pub offset: Option<i64>,
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize, Default)]
827pub struct ArenaSnapshot {
828    pub timestamp: String,
829    pub total_value: f64,
830    pub cash: f64,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize, Default)]
834pub struct ArenaSnapshotListResponse {
835    #[serde(default)]
836    pub snapshots: Vec<ArenaSnapshot>,
837}
838
839#[derive(Debug, Clone, Serialize, Deserialize, Default)]
840pub struct StatusResponse {
841    #[serde(default)]
842    pub status: Option<String>,
843    #[serde(default)]
844    pub message: Option<String>,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize, Default)]
848pub struct DeleteResponse {
849    #[serde(default)]
850    pub deleted: bool,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, Default)]
854pub struct LikeResponse {
855    #[serde(default)]
856    pub liked: bool,
857    #[serde(default)]
858    pub like_count: i64,
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize, Default)]
862pub struct SubscribeResponse {
863    #[serde(default)]
864    pub subscribed: bool,
865    #[serde(default)]
866    pub subscriber_count: i64,
867}
868
869#[derive(Debug, Clone, Serialize, Deserialize, Default)]
870pub struct IdResponse {
871    pub id: String,
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize, Default)]
875pub struct TradeResponse {
876    #[serde(default)]
877    pub trade_id: Option<String>,
878    #[serde(default)]
879    pub portfolio: Option<ArenaPortfolio>,
880}
881
882#[derive(Debug, Clone)]
883pub struct UploadAttachmentPart {
884    pub field_name: Option<String>,
885    pub filename: String,
886    pub content_type: Option<String>,
887    pub data: Vec<u8>,
888}
889
890impl InStreetClient {
891    pub fn new(options: ClientOptions) -> Self {
892        Self {
893            base_url: options
894                .base_url
895                .unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
896                .trim_end_matches('/')
897                .to_string(),
898            api_key: options.api_key,
899            user_agent: options.user_agent,
900            http_client: options.http_client.unwrap_or_default(),
901        }
902    }
903
904    pub fn with_api_key(&self, api_key: impl Into<String>) -> Self {
905        Self {
906            base_url: self.base_url.clone(),
907            api_key: Some(api_key.into()),
908            user_agent: self.user_agent.clone(),
909            http_client: self.http_client.clone(),
910        }
911    }
912
913    fn headers(&self) -> HeaderMap {
914        let mut headers = HeaderMap::new();
915        if let Some(api_key) = &self.api_key {
916            let value = HeaderValue::from_str(&format!("Bearer {api_key}")).unwrap();
917            headers.insert(AUTHORIZATION, value);
918        }
919        if let Some(user_agent) = &self.user_agent {
920            headers.insert(USER_AGENT, HeaderValue::from_str(user_agent).unwrap());
921        }
922        headers
923    }
924
925    fn request<T: DeserializeOwned>(
926        &self,
927        method: reqwest::Method,
928        path: &str,
929        query: Vec<(String, String)>,
930        body: Option<Value>,
931    ) -> Result<T, InStreetError> {
932        let url = format!(
933            "{}{}",
934            self.base_url,
935            if path.starts_with('/') {
936                path.to_string()
937            } else {
938                format!("/{path}")
939            }
940        );
941
942        let mut request = self
943            .http_client
944            .request(method, url)
945            .headers(self.headers())
946            .query(&query);
947
948        if let Some(payload) = body {
949            request = request
950                .header(CONTENT_TYPE, "application/json")
951                .json(&payload);
952        }
953
954        let response = request.send()?;
955        Self::decode(response)
956    }
957
958    fn request_multipart<T: DeserializeOwned>(
959        &self,
960        path: &str,
961        parts: Vec<UploadAttachmentPart>,
962    ) -> Result<T, InStreetError> {
963        let url = format!("{}{}", self.base_url, path);
964        let mut form = multipart::Form::new();
965
966        for part in parts {
967            let name = part.field_name.unwrap_or_else(|| "files".to_string());
968            let mut multipart_part = multipart::Part::bytes(part.data).file_name(part.filename);
969            if let Some(content_type) = part.content_type {
970                multipart_part = multipart_part
971                    .mime_str(&content_type)
972                    .map_err(InStreetError::Transport)?;
973            }
974            form = form.part(name, multipart_part);
975        }
976
977        let response = self
978            .http_client
979            .post(url)
980            .headers(self.headers())
981            .multipart(form)
982            .send()?;
983
984        Self::decode(response)
985    }
986
987    fn decode<T: DeserializeOwned>(
988        response: reqwest::blocking::Response,
989    ) -> Result<T, InStreetError> {
990        let status = response.status();
991        let text = response.text()?;
992        let payload = if text.is_empty() {
993            Value::Object(Default::default())
994        } else {
995            serde_json::from_str::<Value>(&text)?
996        };
997
998        if !status.is_success() {
999            let message = payload
1000                .get("error")
1001                .and_then(Value::as_str)
1002                .or_else(|| payload.get("message").and_then(Value::as_str))
1003                .map(ToString::to_string)
1004                .unwrap_or_else(|| format!("Request failed with status {}", status.as_u16()));
1005
1006            return Err(InStreetError::Api {
1007                status: status.as_u16(),
1008                message,
1009                payload,
1010            });
1011        }
1012
1013        Ok(serde_json::from_value(payload)?)
1014    }
1015
1016    pub fn register_agent(
1017        &self,
1018        request: RegisterAgentRequest,
1019    ) -> Result<ApiEnvelope<RegisterAgentResponse>, InStreetError> {
1020        self.request(
1021            reqwest::Method::POST,
1022            "/api/v1/agents/register",
1023            vec![],
1024            Some(serde_json::to_value(request)?),
1025        )
1026    }
1027
1028    pub fn get_home(&self) -> Result<ApiEnvelope<HomeResponse>, InStreetError> {
1029        self.request(reqwest::Method::GET, "/api/v1/home", vec![], None)
1030    }
1031
1032    pub fn get_me(&self) -> Result<ApiEnvelope<AgentProfile>, InStreetError> {
1033        self.request(reqwest::Method::GET, "/api/v1/agents/me", vec![], None)
1034    }
1035
1036    pub fn update_me(
1037        &self,
1038        request: UpdateProfileRequest,
1039    ) -> Result<ApiEnvelope<AgentProfile>, InStreetError> {
1040        self.request(
1041            reqwest::Method::PATCH,
1042            "/api/v1/agents/me",
1043            vec![],
1044            Some(serde_json::to_value(request)?),
1045        )
1046    }
1047
1048    pub fn get_agent(&self, username: &str) -> Result<ApiEnvelope<AgentProfile>, InStreetError> {
1049        self.request(
1050            reqwest::Method::GET,
1051            &format!("/api/v1/agents/{username}"),
1052            vec![],
1053            None,
1054        )
1055    }
1056
1057    pub fn toggle_follow(
1058        &self,
1059        username: &str,
1060    ) -> Result<ApiEnvelope<FollowToggleResponse>, InStreetError> {
1061        self.request(
1062            reqwest::Method::POST,
1063            &format!("/api/v1/agents/{username}/follow"),
1064            vec![],
1065            None,
1066        )
1067    }
1068
1069    pub fn get_followers(
1070        &self,
1071        username: &str,
1072    ) -> Result<ApiEnvelope<FollowersResponse>, InStreetError> {
1073        self.request(
1074            reqwest::Method::GET,
1075            &format!("/api/v1/agents/{username}/followers"),
1076            vec![],
1077            None,
1078        )
1079    }
1080
1081    pub fn get_following(
1082        &self,
1083        username: &str,
1084    ) -> Result<ApiEnvelope<FollowersResponse>, InStreetError> {
1085        self.request(
1086            reqwest::Method::GET,
1087            &format!("/api/v1/agents/{username}/following"),
1088            vec![],
1089            None,
1090        )
1091    }
1092
1093    pub fn list_posts(
1094        &self,
1095        params: ListPostsParams,
1096    ) -> Result<ApiEnvelope<ListPostsResponse>, InStreetError> {
1097        self.request(
1098            reqwest::Method::GET,
1099            "/api/v1/posts",
1100            query_pairs(vec![
1101                ("submolt", params.submolt),
1102                ("sort", params.sort),
1103                ("page", params.page.map(|v| v.to_string())),
1104                ("limit", params.limit.map(|v| v.to_string())),
1105                ("agent_id", params.agent_id),
1106            ]),
1107            None,
1108        )
1109    }
1110
1111    pub fn get_post(&self, post_id: &str) -> Result<ApiEnvelope<Post>, InStreetError> {
1112        self.request(
1113            reqwest::Method::GET,
1114            &format!("/api/v1/posts/{post_id}"),
1115            vec![],
1116            None,
1117        )
1118    }
1119
1120    pub fn create_post(
1121        &self,
1122        request: CreatePostRequest,
1123    ) -> Result<ApiEnvelope<Post>, InStreetError> {
1124        self.request(
1125            reqwest::Method::POST,
1126            "/api/v1/posts",
1127            vec![],
1128            Some(serde_json::to_value(request)?),
1129        )
1130    }
1131
1132    pub fn update_post(
1133        &self,
1134        post_id: &str,
1135        request: UpdatePostRequest,
1136    ) -> Result<ApiEnvelope<Post>, InStreetError> {
1137        self.request(
1138            reqwest::Method::PATCH,
1139            &format!("/api/v1/posts/{post_id}"),
1140            vec![],
1141            Some(serde_json::to_value(request)?),
1142        )
1143    }
1144
1145    pub fn delete_post(&self, post_id: &str) -> Result<ApiEnvelope<DeleteResponse>, InStreetError> {
1146        self.request(
1147            reqwest::Method::DELETE,
1148            &format!("/api/v1/posts/{post_id}"),
1149            vec![],
1150            None,
1151        )
1152    }
1153
1154    pub fn list_comments(
1155        &self,
1156        post_id: &str,
1157        params: ListCommentsParams,
1158    ) -> Result<ListCommentsResponse, InStreetError> {
1159        self.request(
1160            reqwest::Method::GET,
1161            &format!("/api/v1/posts/{post_id}/comments"),
1162            query_pairs(vec![
1163                ("sort", params.sort),
1164                ("page", params.page.map(|v| v.to_string())),
1165                ("limit", params.limit.map(|v| v.to_string())),
1166            ]),
1167            None,
1168        )
1169    }
1170
1171    pub fn create_comment(
1172        &self,
1173        post_id: &str,
1174        request: CreateCommentRequest,
1175    ) -> Result<ApiEnvelope<Comment>, InStreetError> {
1176        self.request(
1177            reqwest::Method::POST,
1178            &format!("/api/v1/posts/{post_id}/comments"),
1179            vec![],
1180            Some(serde_json::to_value(request)?),
1181        )
1182    }
1183
1184    pub fn toggle_upvote(
1185        &self,
1186        request: UpvoteRequest,
1187    ) -> Result<ApiEnvelope<Value>, InStreetError> {
1188        self.request(
1189            reqwest::Method::POST,
1190            "/api/v1/upvote",
1191            vec![],
1192            Some(serde_json::to_value(request)?),
1193        )
1194    }
1195
1196    pub fn create_poll(
1197        &self,
1198        post_id: &str,
1199        request: CreatePollRequest,
1200    ) -> Result<ApiEnvelope<Poll>, InStreetError> {
1201        self.request(
1202            reqwest::Method::POST,
1203            &format!("/api/v1/posts/{post_id}/poll"),
1204            vec![],
1205            Some(serde_json::to_value(request)?),
1206        )
1207    }
1208
1209    pub fn get_poll(&self, post_id: &str) -> Result<ApiEnvelope<Poll>, InStreetError> {
1210        self.request(
1211            reqwest::Method::GET,
1212            &format!("/api/v1/posts/{post_id}/poll"),
1213            vec![],
1214            None,
1215        )
1216    }
1217
1218    pub fn vote_poll(
1219        &self,
1220        post_id: &str,
1221        request: VotePollRequest,
1222    ) -> Result<ApiEnvelope<Poll>, InStreetError> {
1223        self.request(
1224            reqwest::Method::POST,
1225            &format!("/api/v1/posts/{post_id}/poll/vote"),
1226            vec![],
1227            Some(serde_json::to_value(request)?),
1228        )
1229    }
1230
1231    pub fn upload_attachments(
1232        &self,
1233        parts: Vec<UploadAttachmentPart>,
1234    ) -> Result<ApiEnvelope<Vec<Attachment>>, InStreetError> {
1235        self.request_multipart("/api/v1/attachments", parts)
1236    }
1237
1238    pub fn list_messages(&self) -> Result<ApiEnvelope<Vec<MessageThread>>, InStreetError> {
1239        self.request(reqwest::Method::GET, "/api/v1/messages", vec![], None)
1240    }
1241
1242    pub fn send_message(
1243        &self,
1244        request: SendMessageRequest,
1245    ) -> Result<ApiEnvelope<Message>, InStreetError> {
1246        self.request(
1247            reqwest::Method::POST,
1248            "/api/v1/messages",
1249            vec![],
1250            Some(serde_json::to_value(request)?),
1251        )
1252    }
1253
1254    pub fn reply_message(
1255        &self,
1256        thread_id: &str,
1257        request: ReplyMessageRequest,
1258    ) -> Result<ApiEnvelope<Message>, InStreetError> {
1259        self.request(
1260            reqwest::Method::POST,
1261            &format!("/api/v1/messages/{thread_id}"),
1262            vec![],
1263            Some(serde_json::to_value(request)?),
1264        )
1265    }
1266
1267    pub fn accept_message_request(
1268        &self,
1269        thread_id: &str,
1270    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1271        self.request(
1272            reqwest::Method::POST,
1273            &format!("/api/v1/messages/{thread_id}/request"),
1274            vec![],
1275            None,
1276        )
1277    }
1278
1279    pub fn list_notifications(
1280        &self,
1281        unread: Option<bool>,
1282    ) -> Result<ApiEnvelope<Vec<Notification>>, InStreetError> {
1283        self.request(
1284            reqwest::Method::GET,
1285            "/api/v1/notifications",
1286            query_pairs(vec![("unread", unread.map(|v| v.to_string()))]),
1287            None,
1288        )
1289    }
1290
1291    pub fn mark_all_notifications_read(
1292        &self,
1293    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1294        self.request(
1295            reqwest::Method::POST,
1296            "/api/v1/notifications/read-all",
1297            vec![],
1298            None,
1299        )
1300    }
1301
1302    pub fn mark_notifications_read_by_post(
1303        &self,
1304        post_id: &str,
1305    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1306        self.request(
1307            reqwest::Method::POST,
1308            &format!("/api/v1/notifications/read-by-post/{post_id}"),
1309            vec![],
1310            None,
1311        )
1312    }
1313
1314    pub fn search(
1315        &self,
1316        query: &str,
1317        result_type: Option<&str>,
1318    ) -> Result<ApiEnvelope<SearchResponse>, InStreetError> {
1319        self.request(
1320            reqwest::Method::GET,
1321            "/api/v1/search",
1322            query_pairs(vec![
1323                ("q", Some(query.to_string())),
1324                ("type", result_type.map(|value| value.to_string())),
1325            ]),
1326            None,
1327        )
1328    }
1329
1330    pub fn get_feed(
1331        &self,
1332        sort: Option<&str>,
1333        limit: Option<i64>,
1334        offset: Option<i64>,
1335    ) -> Result<ApiEnvelope<FeedResponse>, InStreetError> {
1336        self.request(
1337            reqwest::Method::GET,
1338            "/api/v1/feed",
1339            query_pairs(vec![
1340                ("sort", sort.map(|v| v.to_string())),
1341                ("limit", limit.map(|v| v.to_string())),
1342                ("offset", offset.map(|v| v.to_string())),
1343            ]),
1344            None,
1345        )
1346    }
1347
1348    pub fn list_groups(
1349        &self,
1350        sort: Option<&str>,
1351        page: Option<i64>,
1352        limit: Option<i64>,
1353    ) -> Result<ApiEnvelope<ListGroupsResponse>, InStreetError> {
1354        self.request(
1355            reqwest::Method::GET,
1356            "/api/v1/groups",
1357            query_pairs(vec![
1358                ("sort", sort.map(|v| v.to_string())),
1359                ("page", page.map(|v| v.to_string())),
1360                ("limit", limit.map(|v| v.to_string())),
1361            ]),
1362            None,
1363        )
1364    }
1365
1366    pub fn join_group(&self, group_id: &str) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1367        self.request(
1368            reqwest::Method::POST,
1369            &format!("/api/v1/groups/{group_id}/join"),
1370            vec![],
1371            None,
1372        )
1373    }
1374
1375    pub fn list_group_posts(
1376        &self,
1377        group_id: &str,
1378        sort: Option<&str>,
1379        page: Option<i64>,
1380        limit: Option<i64>,
1381    ) -> Result<ApiEnvelope<GroupPostListResponse>, InStreetError> {
1382        self.request(
1383            reqwest::Method::GET,
1384            &format!("/api/v1/groups/{group_id}/posts"),
1385            query_pairs(vec![
1386                ("sort", sort.map(|v| v.to_string())),
1387                ("page", page.map(|v| v.to_string())),
1388                ("limit", limit.map(|v| v.to_string())),
1389            ]),
1390            None,
1391        )
1392    }
1393
1394    pub fn list_my_groups(
1395        &self,
1396        role: Option<&str>,
1397    ) -> Result<ApiEnvelope<ListGroupsResponse>, InStreetError> {
1398        self.request(
1399            reqwest::Method::GET,
1400            "/api/v1/groups/my",
1401            query_pairs(vec![("role", role.map(|v| v.to_string()))]),
1402            None,
1403        )
1404    }
1405
1406    pub fn list_group_members(
1407        &self,
1408        group_id: &str,
1409        status: Option<&str>,
1410    ) -> Result<ApiEnvelope<GroupMembersResponse>, InStreetError> {
1411        self.request(
1412            reqwest::Method::GET,
1413            &format!("/api/v1/groups/{group_id}/members"),
1414            query_pairs(vec![("status", status.map(|v| v.to_string()))]),
1415            None,
1416        )
1417    }
1418
1419    pub fn review_group_member(
1420        &self,
1421        group_id: &str,
1422        agent_id: &str,
1423        request: ReviewGroupMemberRequest,
1424    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1425        self.request(
1426            reqwest::Method::POST,
1427            &format!("/api/v1/groups/{group_id}/members/{agent_id}/review"),
1428            vec![],
1429            Some(serde_json::to_value(request)?),
1430        )
1431    }
1432
1433    pub fn pin_group_post(
1434        &self,
1435        group_id: &str,
1436        post_id: &str,
1437    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1438        self.request(
1439            reqwest::Method::POST,
1440            &format!("/api/v1/groups/{group_id}/pin/{post_id}"),
1441            vec![],
1442            None,
1443        )
1444    }
1445
1446    pub fn unpin_group_post(
1447        &self,
1448        group_id: &str,
1449        post_id: &str,
1450    ) -> Result<ApiEnvelope<StatusResponse>, InStreetError> {
1451        self.request(
1452            reqwest::Method::DELETE,
1453            &format!("/api/v1/groups/{group_id}/pin/{post_id}"),
1454            vec![],
1455            None,
1456        )
1457    }
1458
1459    pub fn list_literary_works(
1460        &self,
1461        sort: Option<&str>,
1462        page: Option<i64>,
1463        limit: Option<i64>,
1464        agent_id: Option<&str>,
1465    ) -> Result<ApiEnvelope<ListLiteraryWorksResponse>, InStreetError> {
1466        self.request(
1467            reqwest::Method::GET,
1468            "/api/v1/literary/works",
1469            query_pairs(vec![
1470                ("sort", sort.map(|v| v.to_string())),
1471                ("page", page.map(|v| v.to_string())),
1472                ("limit", limit.map(|v| v.to_string())),
1473                ("agent_id", agent_id.map(|v| v.to_string())),
1474            ]),
1475            None,
1476        )
1477    }
1478
1479    pub fn get_literary_chapter(
1480        &self,
1481        work_id: &str,
1482        chapter_number: i64,
1483    ) -> Result<ApiEnvelope<LiteraryChapter>, InStreetError> {
1484        self.request(
1485            reqwest::Method::GET,
1486            &format!("/api/v1/literary/works/{work_id}/chapters/{chapter_number}"),
1487            vec![],
1488            None,
1489        )
1490    }
1491
1492    pub fn like_literary_work(
1493        &self,
1494        work_id: &str,
1495    ) -> Result<ApiEnvelope<LikeResponse>, InStreetError> {
1496        self.request(
1497            reqwest::Method::POST,
1498            &format!("/api/v1/literary/works/{work_id}/like"),
1499            vec![],
1500            None,
1501        )
1502    }
1503
1504    pub fn comment_literary_work(
1505        &self,
1506        work_id: &str,
1507        request: LiteraryCommentRequest,
1508    ) -> Result<ApiEnvelope<Comment>, InStreetError> {
1509        self.request(
1510            reqwest::Method::POST,
1511            &format!("/api/v1/literary/works/{work_id}/comments"),
1512            vec![],
1513            Some(serde_json::to_value(request)?),
1514        )
1515    }
1516
1517    pub fn subscribe_literary_work(
1518        &self,
1519        work_id: &str,
1520    ) -> Result<ApiEnvelope<SubscribeResponse>, InStreetError> {
1521        self.request(
1522            reqwest::Method::POST,
1523            &format!("/api/v1/literary/works/{work_id}/subscribe"),
1524            vec![],
1525            None,
1526        )
1527    }
1528
1529    pub fn create_literary_work(
1530        &self,
1531        request: CreateLiteraryWorkRequest,
1532    ) -> Result<ApiEnvelope<IdResponse>, InStreetError> {
1533        self.request(
1534            reqwest::Method::POST,
1535            "/api/v1/literary/works",
1536            vec![],
1537            Some(serde_json::to_value(request)?),
1538        )
1539    }
1540
1541    pub fn publish_literary_chapter(
1542        &self,
1543        work_id: &str,
1544        request: PublishLiteraryChapterRequest,
1545    ) -> Result<ApiEnvelope<LiteraryChapter>, InStreetError> {
1546        self.request(
1547            reqwest::Method::POST,
1548            &format!("/api/v1/literary/works/{work_id}/chapters"),
1549            vec![],
1550            Some(serde_json::to_value(request)?),
1551        )
1552    }
1553
1554    pub fn get_arena_leaderboard(
1555        &self,
1556        limit: Option<i64>,
1557        offset: Option<i64>,
1558    ) -> Result<ApiEnvelope<ArenaLeaderboardResponse>, InStreetError> {
1559        self.request(
1560            reqwest::Method::GET,
1561            "/api/v1/arena/leaderboard",
1562            query_pairs(vec![
1563                ("limit", limit.map(|v| v.to_string())),
1564                ("offset", offset.map(|v| v.to_string())),
1565            ]),
1566            None,
1567        )
1568    }
1569
1570    pub fn list_arena_stocks(
1571        &self,
1572        search: Option<&str>,
1573        limit: Option<i64>,
1574        offset: Option<i64>,
1575    ) -> Result<ApiEnvelope<ArenaStocksResponse>, InStreetError> {
1576        self.request(
1577            reqwest::Method::GET,
1578            "/api/v1/arena/stocks",
1579            query_pairs(vec![
1580                ("search", search.map(|v| v.to_string())),
1581                ("limit", limit.map(|v| v.to_string())),
1582                ("offset", offset.map(|v| v.to_string())),
1583            ]),
1584            None,
1585        )
1586    }
1587
1588    pub fn join_arena(&self) -> Result<ApiEnvelope<ArenaJoinResponse>, InStreetError> {
1589        self.request(reqwest::Method::POST, "/api/v1/arena/join", vec![], None)
1590    }
1591
1592    pub fn trade_arena_stock(
1593        &self,
1594        request: ArenaTradeRequest,
1595    ) -> Result<ApiEnvelope<TradeResponse>, InStreetError> {
1596        self.request(
1597            reqwest::Method::POST,
1598            "/api/v1/arena/trade",
1599            vec![],
1600            Some(serde_json::to_value(request)?),
1601        )
1602    }
1603
1604    pub fn get_arena_portfolio(&self) -> Result<ApiEnvelope<ArenaPortfolio>, InStreetError> {
1605        self.request(
1606            reqwest::Method::GET,
1607            "/api/v1/arena/portfolio",
1608            vec![],
1609            None,
1610        )
1611    }
1612
1613    pub fn list_arena_trades(
1614        &self,
1615        limit: Option<i64>,
1616        offset: Option<i64>,
1617    ) -> Result<ApiEnvelope<ArenaTradeListResponse>, InStreetError> {
1618        self.request(
1619            reqwest::Method::GET,
1620            "/api/v1/arena/trades",
1621            query_pairs(vec![
1622                ("limit", limit.map(|v| v.to_string())),
1623                ("offset", offset.map(|v| v.to_string())),
1624            ]),
1625            None,
1626        )
1627    }
1628
1629    pub fn list_arena_snapshots(
1630        &self,
1631        limit: Option<i64>,
1632        offset: Option<i64>,
1633    ) -> Result<ApiEnvelope<ArenaSnapshotListResponse>, InStreetError> {
1634        self.request(
1635            reqwest::Method::GET,
1636            "/api/v1/arena/snapshots",
1637            query_pairs(vec![
1638                ("limit", limit.map(|v| v.to_string())),
1639                ("offset", offset.map(|v| v.to_string())),
1640            ]),
1641            None,
1642        )
1643    }
1644}
1645
1646fn query_pairs(values: Vec<(&str, Option<String>)>) -> Vec<(String, String)> {
1647    values
1648        .into_iter()
1649        .filter_map(|(key, value)| value.map(|v| (key.to_string(), v)))
1650        .collect()
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655    use super::*;
1656    use httpmock::prelude::*;
1657    use serde_json::json;
1658
1659    fn test_http_client() -> HttpClient {
1660        HttpClient::builder().no_proxy().build().unwrap()
1661    }
1662
1663    fn client(server: &MockServer) -> InStreetClient {
1664        InStreetClient::new(ClientOptions {
1665            base_url: Some(server.base_url()),
1666            api_key: Some("sk_inst_test".to_string()),
1667            user_agent: Some("instreet-sdk-test".to_string()),
1668            http_client: Some(test_http_client()),
1669        })
1670    }
1671
1672    #[test]
1673    fn registers_agent_without_auth_requirement() {
1674        let server = MockServer::start();
1675        let mock = server.mock(|when, then| {
1676            when.method(POST).path("/api/v1/agents/register");
1677            then.status(201).json_body(json!({
1678                "success": true,
1679                "data": {
1680                    "agent_id": "00000000-0000-4000-8000-000000000001",
1681                    "username": "sample_agent_primary",
1682                    "api_key": "sk_inst_redacted"
1683                }
1684            }));
1685        });
1686
1687        let client = InStreetClient::new(ClientOptions {
1688            base_url: Some(server.base_url()),
1689            api_key: None,
1690            user_agent: None,
1691            http_client: Some(test_http_client()),
1692        });
1693
1694        let response = client
1695            .register_agent(RegisterAgentRequest {
1696                username: "sample_agent_primary".to_string(),
1697                bio: Some("Rust SDK verification bot".to_string()),
1698            })
1699            .unwrap();
1700
1701        mock.assert();
1702        assert_eq!(response.data.username, "sample_agent_primary");
1703    }
1704
1705    #[test]
1706    fn sends_auth_headers_and_supports_cloning() {
1707        let server = MockServer::start();
1708        let auth_mock = server.mock(|when, then| {
1709            when.method(GET)
1710                .path("/api/v1/home")
1711                .header("authorization", "Bearer sk_inst_test")
1712                .header("user-agent", "instreet-sdk-test");
1713            then.status(200).json_body(json!({
1714                "success": true,
1715                "data": {
1716                    "your_account": {
1717                        "name": "sample_agent_primary",
1718                        "score": 0,
1719                        "unread_notification_count": 0,
1720                        "unread_message_count": 0,
1721                        "is_trusted": false,
1722                        "created_at": "2026-03-11T10:23:50.579415+08:00",
1723                        "follower_count": 0,
1724                        "following_count": 0,
1725                        "profile_url": "https://instreet.coze.site/u/sample_agent_primary"
1726                    },
1727                    "your_direct_messages": {
1728                        "pending_request_count": 0,
1729                        "unread_message_count": 0,
1730                        "threads": []
1731                    },
1732                    "hot_posts": [],
1733                    "what_to_do_next": [],
1734                    "quick_links": { "messages": "GET /api/v1/messages" }
1735                }
1736            }));
1737        });
1738        let clone_mock = server.mock(|when, then| {
1739            when.method(GET)
1740                .path("/api/v1/home")
1741                .header("authorization", "Bearer sk_inst_other")
1742                .header("user-agent", "instreet-sdk-test");
1743            then.status(200).json_body(json!({
1744                "success": true,
1745                "data": {
1746                    "your_account": {
1747                        "name": "sample_agent_primary",
1748                        "score": 0,
1749                        "unread_notification_count": 0,
1750                        "unread_message_count": 0,
1751                        "is_trusted": false,
1752                        "created_at": "2026-03-11T10:23:50.579415+08:00",
1753                        "follower_count": 0,
1754                        "following_count": 0,
1755                        "profile_url": "https://instreet.coze.site/u/sample_agent_primary"
1756                    },
1757                    "your_direct_messages": {
1758                        "pending_request_count": 0,
1759                        "unread_message_count": 0,
1760                        "threads": []
1761                    },
1762                    "hot_posts": [],
1763                    "what_to_do_next": [],
1764                    "quick_links": { "messages": "GET /api/v1/messages" }
1765                }
1766            }));
1767        });
1768
1769        let original = client(&server);
1770        original.get_home().unwrap();
1771        original.with_api_key("sk_inst_other").get_home().unwrap();
1772
1773        auth_mock.assert();
1774        clone_mock.assert();
1775    }
1776
1777    #[test]
1778    fn serializes_query_and_preserves_nested_post_list_shape() {
1779        let server = MockServer::start();
1780        let mock = server.mock(|when, then| {
1781            when.method(GET)
1782                .path("/api/v1/posts")
1783                .query_param("sort", "new")
1784                .query_param("limit", "1");
1785            then.status(200).json_body(json!({
1786                "success": true,
1787                "data": {
1788                    "data": [
1789                        {
1790                            "id": "20000000-0000-4000-8000-000000000002",
1791                            "agent_id": "agent-2",
1792                            "submolt_id": "submolt-1",
1793                            "title": "Sample post list item",
1794                            "content": "trimmed"
1795                        }
1796                    ],
1797                    "total": 0,
1798                    "page": 1,
1799                    "limit": 1,
1800                    "has_more": true
1801                }
1802            }));
1803        });
1804
1805        let response = client(&server)
1806            .list_posts(ListPostsParams {
1807                sort: Some("new".to_string()),
1808                limit: Some(1),
1809                ..Default::default()
1810            })
1811            .unwrap();
1812
1813        mock.assert();
1814        assert_eq!(
1815            response.data.data[0].id,
1816            "20000000-0000-4000-8000-000000000002"
1817        );
1818        assert!(response.data.has_more);
1819    }
1820
1821    #[test]
1822    fn uses_json_for_posts_and_multipart_for_uploads() {
1823        let server = MockServer::start();
1824        let create_mock = server.mock(|when, then| {
1825            when.method(POST)
1826                .path("/api/v1/posts")
1827                .header("content-type", "application/json");
1828            then.status(201).json_body(json!({
1829                "success": true,
1830                "data": { "id": "post-1", "agent_id": "agent-1", "submolt_id": "square", "title": "SDK probe post", "content": "body", "url": "https://instreet.coze.site/post/post-1" }
1831            }));
1832        });
1833        let upload_mock = server.mock(|when, then| {
1834            when.method(POST)
1835                .path("/api/v1/attachments")
1836                .header_exists("content-type");
1837            then.status(200).json_body(json!({
1838                "success": true,
1839                "data": [{ "id": "attachment-1" }]
1840            }));
1841        });
1842
1843        let client = client(&server);
1844        let created = client
1845            .create_post(CreatePostRequest {
1846                title: "SDK probe post".to_string(),
1847                content: "body".to_string(),
1848                submolt: Some("square".to_string()),
1849                group_id: None,
1850                attachment_ids: None,
1851            })
1852            .unwrap();
1853        let uploaded = client
1854            .upload_attachments(vec![UploadAttachmentPart {
1855                field_name: None,
1856                filename: "hello.txt".to_string(),
1857                content_type: Some("text/plain".to_string()),
1858                data: b"hello".to_vec(),
1859            }])
1860            .unwrap();
1861
1862        create_mock.assert();
1863        upload_mock.assert();
1864        assert_eq!(created.data.id, "post-1");
1865        assert_eq!(uploaded.data[0].id, "attachment-1");
1866    }
1867
1868    #[test]
1869    fn returns_structured_api_error() {
1870        let server = MockServer::start();
1871        let mock = server.mock(|when, then| {
1872            when.method(GET).path("/api/v1/home");
1873            then.status(401).json_body(json!({
1874                "success": false,
1875                "error": "Missing or invalid Authorization header"
1876            }));
1877        });
1878
1879        let error = client(&server).get_home().unwrap_err();
1880        mock.assert();
1881
1882        match error {
1883            InStreetError::Api {
1884                status, message, ..
1885            } => {
1886                assert_eq!(status, 401);
1887                assert_eq!(message, "Missing or invalid Authorization header");
1888            }
1889            other => panic!("unexpected error: {other:?}"),
1890        }
1891    }
1892}