Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ecb47ed

Browse files
huacnleehogan-yuanclaude
authored
feat(content): add topic_detail, list_topic_replies, create_topic_reply (longbridge#499)
## Summary - **types**: add `ListTopicRepliesOptions`, `CreateReplyOptions`, `TopicReply` (with author, images, engagement counts, `created_at`) - **context (async)**: add `topic_detail`, `list_topic_replies`, `create_topic_reply` methods - **blocking**: add sync wrappers for the three new methods - **python bindings**: add `TopicReply` pyclass; sync + async bindings for all three methods in `ContentContext` / `AsyncContentContext` - **doc comments**: update `create_topic` and `create_topic_reply` with permission requirements, tickers auto-link warning, and rate-limit details ## Test plan - [ ] `cargo build` passes for both `longbridge` and `longbridge-python` crates - [ ] Verify `TopicReply` fields are accessible from Python 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: 袁章洪 <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
1 parent c538b8b commit ecb47ed

8 files changed

Lines changed: 309 additions & 13 deletions

File tree

python/src/content/context.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use std::sync::Arc;
22

33
use longbridge::{
44
blocking::ContentContextSync,
5-
content::{CreateTopicOptions, MyTopicsOptions},
5+
content::{CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions},
66
};
77
use pyo3::prelude::*;
88

99
use crate::{
1010
config::Config,
11-
content::types::{NewsItem, OwnedTopic, TopicItem},
11+
content::types::{NewsItem, OwnedTopic, TopicItem, TopicReply},
1212
error::ErrorNewType,
1313
};
1414

@@ -46,7 +46,9 @@ impl ContentContext {
4646
.collect()
4747
}
4848

49-
/// Create a new topic
49+
/// Create a new community topic.
50+
///
51+
/// See: <https://open.longbridge.com/docs/api?op=create_topic>
5052
#[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))]
5153
pub fn create_topic(
5254
&self,
@@ -87,4 +89,41 @@ impl ContentContext {
8789
.map(TryInto::try_into)
8890
.collect()
8991
}
92+
93+
/// Get full details of a topic by its ID
94+
pub fn topic_detail(&self, id: String) -> PyResult<OwnedTopic> {
95+
self.ctx.topic_detail(id).map_err(ErrorNewType)?.try_into()
96+
}
97+
98+
/// List replies on a topic
99+
#[pyo3(signature = (topic_id, page = None, size = None))]
100+
pub fn list_topic_replies(
101+
&self,
102+
topic_id: String,
103+
page: Option<i32>,
104+
size: Option<i32>,
105+
) -> PyResult<Vec<TopicReply>> {
106+
self.ctx
107+
.list_topic_replies(topic_id, ListTopicRepliesOptions { page, size })
108+
.map_err(ErrorNewType)?
109+
.into_iter()
110+
.map(TryInto::try_into)
111+
.collect()
112+
}
113+
114+
/// Post a reply to a community topic.
115+
///
116+
/// See: <https://open.longbridge.com/docs/api?op=create_topic_reply>
117+
#[pyo3(signature = (topic_id, body, reply_to_id = None))]
118+
pub fn create_topic_reply(
119+
&self,
120+
topic_id: String,
121+
body: String,
122+
reply_to_id: Option<String>,
123+
) -> PyResult<TopicReply> {
124+
self.ctx
125+
.create_topic_reply(topic_id, CreateReplyOptions { body, reply_to_id })
126+
.map_err(ErrorNewType)?
127+
.try_into()
128+
}
90129
}

python/src/content/context_async.rs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use std::sync::Arc;
22

3-
use longbridge::content::{ContentContext, CreateTopicOptions, MyTopicsOptions};
3+
use longbridge::content::{
4+
ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions,
5+
MyTopicsOptions,
6+
};
47
use pyo3::{prelude::*, types::PyType};
58

69
use crate::{
710
config::Config,
8-
content::types::{NewsItem, OwnedTopic, TopicItem},
11+
content::types::{NewsItem, OwnedTopic, TopicItem, TopicReply},
912
error::ErrorNewType,
1013
};
1114

@@ -51,7 +54,9 @@ impl AsyncContentContext {
5154
.map(|b| b.unbind())
5255
}
5356

54-
/// Create a new topic. Returns awaitable.
57+
/// Create a new community topic. Returns awaitable.
58+
///
59+
/// See: <https://open.longbridge.com/docs/api?op=create_topic>
5560
#[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))]
5661
fn create_topic(
5762
&self,
@@ -101,4 +106,58 @@ impl AsyncContentContext {
101106
})
102107
.map(|b| b.unbind())
103108
}
109+
110+
/// Get full details of a topic by its ID. Returns awaitable.
111+
fn topic_detail(&self, py: Python<'_>, id: String) -> PyResult<Py<PyAny>> {
112+
let ctx = self.ctx.clone();
113+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
114+
let v = ctx.topic_detail(id).await.map_err(ErrorNewType)?;
115+
OwnedTopic::try_from(v)
116+
})
117+
.map(|b| b.unbind())
118+
}
119+
120+
/// List replies on a topic. Returns awaitable.
121+
#[pyo3(signature = (topic_id, page = None, size = None))]
122+
fn list_topic_replies(
123+
&self,
124+
py: Python<'_>,
125+
topic_id: String,
126+
page: Option<i32>,
127+
size: Option<i32>,
128+
) -> PyResult<Py<PyAny>> {
129+
let ctx = self.ctx.clone();
130+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
131+
let v = ctx
132+
.list_topic_replies(topic_id, ListTopicRepliesOptions { page, size })
133+
.await
134+
.map_err(ErrorNewType)?;
135+
v.into_iter()
136+
.map(|x| -> PyResult<TopicReply> { x.try_into() })
137+
.collect::<PyResult<Vec<TopicReply>>>()
138+
})
139+
.map(|b| b.unbind())
140+
}
141+
142+
/// Post a reply to a community topic. Returns awaitable.
143+
///
144+
/// See: <https://open.longbridge.com/docs/api?op=create_topic_reply>
145+
#[pyo3(signature = (topic_id, body, reply_to_id = None))]
146+
fn create_topic_reply(
147+
&self,
148+
py: Python<'_>,
149+
topic_id: String,
150+
body: String,
151+
reply_to_id: Option<String>,
152+
) -> PyResult<Py<PyAny>> {
153+
let ctx = self.ctx.clone();
154+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
155+
let v = ctx
156+
.create_topic_reply(topic_id, CreateReplyOptions { body, reply_to_id })
157+
.await
158+
.map_err(ErrorNewType)?;
159+
TopicReply::try_from(v)
160+
})
161+
.map(|b| b.unbind())
162+
}
104163
}

python/src/content/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(crate) fn register_types(parent: &Bound<PyModule>) -> PyResult<()> {
1010
parent.add_class::<types::TopicAuthor>()?;
1111
parent.add_class::<types::TopicImage>()?;
1212
parent.add_class::<types::OwnedTopic>()?;
13+
parent.add_class::<types::TopicReply>()?;
1314
parent.add_class::<context::ContentContext>()?;
1415
parent.add_class::<context_async::AsyncContentContext>()?;
1516
Ok(())

python/src/content/types.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ pub(crate) struct TopicItem {
9494
shares_count: i32,
9595
}
9696

97+
/// A reply on a topic
98+
#[pyclass(skip_from_py_object)]
99+
#[derive(Debug, PyObject, Clone)]
100+
#[py(remote = "longbridge::content::TopicReply")]
101+
pub(crate) struct TopicReply {
102+
/// Reply ID
103+
id: String,
104+
/// Topic ID this reply belongs to
105+
topic_id: String,
106+
/// Reply body (plain text)
107+
body: String,
108+
/// ID of the parent reply ("0" means top-level)
109+
reply_to_id: String,
110+
/// Author info
111+
author: TopicAuthor,
112+
/// Attached images
113+
#[py(array)]
114+
images: Vec<TopicImage>,
115+
/// Likes count
116+
likes_count: i32,
117+
/// Nested replies count
118+
comments_count: i32,
119+
/// Created time
120+
created_at: PyOffsetDateTimeWrapper,
121+
}
122+
97123
/// News item
98124
#[pyclass(skip_from_py_object)]
99125
#[derive(Debug, PyObject, Clone)]

rust/src/blocking/content.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use crate::{
66
Config, Result,
77
blocking::runtime::BlockingRuntime,
88
content::{
9-
ContentContext, CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem,
9+
ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions,
10+
MyTopicsOptions, NewsItem, OwnedTopic, TopicItem, TopicReply,
1011
},
1112
};
1213

@@ -55,4 +56,33 @@ impl ContentContextSync {
5556
self.rt
5657
.call(move |ctx| async move { ctx.news(symbol).await })
5758
}
59+
60+
/// Get full details of a topic by its ID
61+
pub fn topic_detail(&self, id: impl Into<String>) -> Result<OwnedTopic> {
62+
let id = id.into();
63+
self.rt
64+
.call(move |ctx| async move { ctx.topic_detail(id).await })
65+
}
66+
67+
/// List replies on a topic
68+
pub fn list_topic_replies(
69+
&self,
70+
topic_id: impl Into<String>,
71+
opts: ListTopicRepliesOptions,
72+
) -> Result<Vec<TopicReply>> {
73+
let topic_id = topic_id.into();
74+
self.rt
75+
.call(move |ctx| async move { ctx.list_topic_replies(topic_id, opts).await })
76+
}
77+
78+
/// Post a reply to a topic
79+
pub fn create_topic_reply(
80+
&self,
81+
topic_id: impl Into<String>,
82+
opts: CreateReplyOptions,
83+
) -> Result<TopicReply> {
84+
let topic_id = topic_id.into();
85+
self.rt
86+
.call(move |ctx| async move { ctx.create_topic_reply(topic_id, opts).await })
87+
}
5888
}

rust/src/content/context.rs

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::sync::Arc;
33
use longbridge_httpcli::{HttpClient, Json, Method};
44
use serde::Deserialize;
55

6-
use super::types::{CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem};
6+
use super::types::{
7+
CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, NewsItem,
8+
OwnedTopic, TopicItem, TopicReply,
9+
};
710
use crate::{Config, Result};
811

912
struct InnerContentContext {
@@ -22,9 +25,9 @@ impl ContentContext {
2225
}))
2326
}
2427

25-
/// Get topics created by the current authenticated user
28+
/// Get topics created by the current authenticated user.
2629
///
27-
/// Path: GET /v1/content/topics/mine
30+
/// See: <https://open.longbridge.com/docs/api?op=list_my_topics>
2831
pub async fn my_topics(&self, opts: MyTopicsOptions) -> Result<Vec<OwnedTopic>> {
2932
#[derive(Debug, Deserialize)]
3033
struct Response {
@@ -43,9 +46,9 @@ impl ContentContext {
4346
.items)
4447
}
4548

46-
/// Create a new topic
49+
/// Create a new community topic.
4750
///
48-
/// Path: POST /v1/content/topics
51+
/// See: <https://open.longbridge.com/docs/api?op=create_topic>
4952
pub async fn create_topic(&self, opts: CreateTopicOptions) -> Result<String> {
5053
#[derive(Debug, Deserialize)]
5154
struct TopicId {
@@ -89,6 +92,85 @@ impl ContentContext {
8992
.items)
9093
}
9194

95+
/// Get full details of a topic by its ID.
96+
///
97+
/// See: <https://open.longbridge.com/docs/api?op=topic_detail>
98+
pub async fn topic_detail(&self, id: impl Into<String>) -> Result<OwnedTopic> {
99+
#[derive(Debug, Deserialize)]
100+
struct Response {
101+
item: OwnedTopic,
102+
}
103+
104+
let id = id.into();
105+
Ok(self
106+
.0
107+
.http_cli
108+
.request(Method::GET, format!("/v1/content/topics/{id}"))
109+
.response::<Json<Response>>()
110+
.send()
111+
.await?
112+
.0
113+
.item)
114+
}
115+
116+
/// List replies on a topic.
117+
///
118+
/// See: <https://open.longbridge.com/docs/api?op=list_topic_replies>
119+
pub async fn list_topic_replies(
120+
&self,
121+
topic_id: impl Into<String>,
122+
opts: ListTopicRepliesOptions,
123+
) -> Result<Vec<TopicReply>> {
124+
#[derive(Debug, Deserialize)]
125+
struct Response {
126+
items: Vec<TopicReply>,
127+
}
128+
129+
let topic_id = topic_id.into();
130+
Ok(self
131+
.0
132+
.http_cli
133+
.request(
134+
Method::GET,
135+
format!("/v1/content/topics/{topic_id}/comments"),
136+
)
137+
.query_params(opts)
138+
.response::<Json<Response>>()
139+
.send()
140+
.await?
141+
.0
142+
.items)
143+
}
144+
145+
/// Post a reply to a community topic.
146+
///
147+
/// See: <https://open.longbridge.com/docs/api?op=create_topic_reply>
148+
pub async fn create_topic_reply(
149+
&self,
150+
topic_id: impl Into<String>,
151+
opts: CreateReplyOptions,
152+
) -> Result<TopicReply> {
153+
#[derive(Debug, Deserialize)]
154+
struct Response {
155+
item: TopicReply,
156+
}
157+
158+
let topic_id = topic_id.into();
159+
Ok(self
160+
.0
161+
.http_cli
162+
.request(
163+
Method::POST,
164+
format!("/v1/content/topics/{topic_id}/comments"),
165+
)
166+
.body(Json(opts))
167+
.response::<Json<Response>>()
168+
.send()
169+
.await?
170+
.0
171+
.item)
172+
}
173+
92174
/// Get news list
93175
pub async fn news(&self, symbol: impl Into<String>) -> Result<Vec<NewsItem>> {
94176
#[derive(Debug, Deserialize)]

rust/src/content/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ mod types;
55

66
pub use context::ContentContext;
77
pub use types::{
8-
CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, TopicItem,
8+
CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, NewsItem,
9+
OwnedTopic, TopicAuthor, TopicImage, TopicItem, TopicReply,
910
};

0 commit comments

Comments
 (0)