From e5f28677b3296095f0dbaf836bf7808a56926f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 26 Mar 2026 10:57:09 +0800 Subject: [PATCH 1/8] refactor: rename topics_mine to my_topics and remove license field - Rename topics_mine -> my_topics across all SDKs (Rust, C, C++, Python, Node.js, Java) - Rename ListMyTopicsOptions -> MyTopicsOptions across all SDKs - Remove license field from OwnedTopic and CreateTopicOptions across all SDKs --- .gitignore | 3 +- c/csrc/include/longbridge.h | 6 ---- c/src/content_context/context.rs | 10 ++----- c/src/content_context/types.rs | 5 ---- cpp/include/content_context.hpp | 2 +- cpp/include/types.hpp | 6 +--- cpp/src/content_context.cpp | 7 ++--- cpp/src/convert.hpp | 1 - .../main/java/com/longbridge/SdkNative.java | 2 +- .../longbridge/content/ContentContext.java | 6 ++-- .../content/CreateTopicOptions.java | 11 -------- ...opicsOptions.java => MyTopicsOptions.java} | 8 +++--- .../com/longbridge/content/OwnedTopic.java | 4 --- java/src/content_context.rs | 8 ++---- java/src/types/classes.rs | 1 - nodejs/index.d.ts | 28 ++++++++----------- nodejs/src/content/context.rs | 6 ++-- nodejs/src/content/requests.rs | 14 ++++------ nodejs/src/content/types.rs | 2 -- python/src/content/context.rs | 10 +++---- python/src/content/context_async.rs | 10 +++---- python/src/content/types.rs | 2 -- rust/src/blocking/content.rs | 6 ++-- rust/src/content/context.rs | 4 +-- rust/src/content/mod.rs | 2 +- rust/src/content/types.rs | 11 ++++---- 26 files changed, 60 insertions(+), 115 deletions(-) rename java/javasrc/src/main/java/com/longbridge/content/{ListMyTopicsOptions.java => MyTopicsOptions.java} (81%) diff --git a/.gitignore b/.gitignore index 83f0b27f8..83e7f6f38 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ build .DS_Store -*/.DS_Store \ No newline at end of file +*/.DS_Store +*.dylib diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 5a583ac74..cb09f4795 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -4013,10 +4013,6 @@ typedef struct lb_owned_topic_t { * Content type: "article" or "post" */ const char *topic_type; - /** - * License: 0=none, 1=original, 2=non-original - */ - int32_t license; /** * URL to the full topic page */ @@ -4273,7 +4269,6 @@ void lb_content_context_topics_mine(const struct lb_content_context_t *ctx, * @param num_tickers Number of tickers * @param hashtags Hashtag names array (NULL = none) * @param num_hashtags Number of hashtags - * @param license 0=none, 1=original, 2=non-original (-1 = default) * @param callback Async callback * @param userdata User data passed to the callback */ @@ -4285,7 +4280,6 @@ void lb_content_context_create_topic(const struct lb_content_context_t *ctx, uintptr_t num_tickers, const char *const *hashtags, uintptr_t num_hashtags, - int32_t license, lb_async_callback_t callback, void *userdata); diff --git a/c/src/content_context/context.rs b/c/src/content_context/context.rs index 6a4d44bc0..154850806 100644 --- a/c/src/content_context/context.rs +++ b/c/src/content_context/context.rs @@ -1,6 +1,6 @@ use std::{ffi::c_void, os::raw::c_char, sync::Arc}; -use longbridge::content::{ContentContext, CreateTopicOptions, ListMyTopicsOptions}; +use longbridge::content::{ContentContext, CreateTopicOptions, MyTopicsOptions}; use crate::{ async_call::{CAsyncCallback, execute_async}, @@ -46,7 +46,7 @@ pub unsafe extern "C" fn lb_content_context_release(ctx: *const CContentContext) /// @param callback Async callback /// @param userdata User data passed to the callback #[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_content_context_topics_mine( +pub unsafe extern "C" fn lb_content_context_my_topics( ctx: *const CContentContext, page: i32, size: i32, @@ -62,7 +62,7 @@ pub unsafe extern "C" fn lb_content_context_topics_mine( }; execute_async(callback, ctx, userdata, async move { let rows: CVec = ctx_inner - .topics_mine(ListMyTopicsOptions { + .my_topics(MyTopicsOptions { page: if page > 0 { Some(page) } else { None }, size: if size > 0 { Some(size) } else { None }, topic_type, @@ -83,7 +83,6 @@ pub unsafe extern "C" fn lb_content_context_topics_mine( /// @param num_tickers Number of tickers /// @param hashtags Hashtag names array (NULL = none) /// @param num_hashtags Number of hashtags -/// @param license 0=none, 1=original, 2=non-original (-1 = default) /// @param callback Async callback /// @param userdata User data passed to the callback #[unsafe(no_mangle)] @@ -96,7 +95,6 @@ pub unsafe extern "C" fn lb_content_context_create_topic( num_tickers: usize, hashtags: *const *const c_char, num_hashtags: usize, - license: i32, callback: CAsyncCallback, userdata: *mut c_void, ) { @@ -118,7 +116,6 @@ pub unsafe extern "C" fn lb_content_context_create_topic( } else { Some(cstr_array_to_rust(hashtags, num_hashtags)) }; - let license = if license >= 0 { Some(license) } else { None }; execute_async(callback, ctx, userdata, async move { let id = ctx_inner .create_topic(CreateTopicOptions { @@ -127,7 +124,6 @@ pub unsafe extern "C" fn lb_content_context_create_topic( topic_type, tickers, hashtags, - license, }) .await?; Ok(CString::from(id)) diff --git a/c/src/content_context/types.rs b/c/src/content_context/types.rs index 30761e9e9..e9b86dfc4 100644 --- a/c/src/content_context/types.rs +++ b/c/src/content_context/types.rs @@ -117,8 +117,6 @@ pub struct COwnedTopic { pub shares_count: i32, /// Content type: "article" or "post" pub topic_type: *const c_char, - /// License: 0=none, 1=original, 2=non-original - pub license: i32, /// URL to the full topic page pub detail_url: *const c_char, /// Created time (Unix timestamp) @@ -142,7 +140,6 @@ pub(crate) struct COwnedTopicOwned { views_count: i32, shares_count: i32, topic_type: CString, - license: i32, detail_url: CString, created_at: i64, updated_at: i64, @@ -164,7 +161,6 @@ impl From for COwnedTopicOwned { views_count: item.views_count, shares_count: item.shares_count, topic_type: item.topic_type.into(), - license: item.license, detail_url: item.detail_url.into(), created_at: item.created_at.unix_timestamp(), updated_at: item.updated_at.unix_timestamp(), @@ -192,7 +188,6 @@ impl ToFFI for COwnedTopicOwned { views_count: self.views_count, shares_count: self.shares_count, topic_type: self.topic_type.to_ffi_type(), - license: self.license, detail_url: self.detail_url.to_ffi_type(), created_at: self.created_at, updated_at: self.updated_at, diff --git a/cpp/include/content_context.hpp b/cpp/include/content_context.hpp index 6ce2562f5..688af8efa 100644 --- a/cpp/include/content_context.hpp +++ b/cpp/include/content_context.hpp @@ -28,7 +28,7 @@ class ContentContext static ContentContext create(const Config& config); /// Get topics created by the current authenticated user - void topics_mine(const ListMyTopicsOptions& opts, + void my_topics(const MyTopicsOptions& opts, AsyncCallback> callback) const; /// Create a new topic diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 503e327e4..2773c1a9c 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -2178,8 +2178,6 @@ struct OwnedTopic int32_t shares_count; /// Content type: "article" or "post" std::string topic_type; - /// License: 0=none, 1=original, 2=non-original - int32_t license; /// URL to the full topic page std::string detail_url; /// Created time (Unix timestamp) @@ -2189,7 +2187,7 @@ struct OwnedTopic }; /// Options for listing topics created by the current authenticated user -struct ListMyTopicsOptions +struct MyTopicsOptions { /// Page number (0 = default 1) int32_t page = 0; @@ -2212,8 +2210,6 @@ struct CreateTopicOptions std::vector tickers; /// Hashtag names, max 5 std::vector hashtags; - /// License: 0=none (default), 1=original, 2=non-original (-1 = not set) - int32_t license = -1; }; } // namespace content diff --git a/cpp/src/content_context.cpp b/cpp/src/content_context.cpp index 78b30c7d7..0c65f8ef6 100644 --- a/cpp/src/content_context.cpp +++ b/cpp/src/content_context.cpp @@ -64,13 +64,13 @@ ContentContext::create(const Config& config) } void -ContentContext::topics_mine( - const ListMyTopicsOptions& opts, +ContentContext::my_topics( + const MyTopicsOptions& opts, AsyncCallback> callback) const { const char* topic_type = opts.topic_type.empty() ? nullptr : opts.topic_type.c_str(); - lb_content_context_topics_mine( + lb_content_context_my_topics( ctx_, opts.page, opts.size, @@ -126,7 +126,6 @@ ContentContext::create_topic( tickers_cstr.size(), hashtags_cstr.empty() ? nullptr : hashtags_cstr.data(), hashtags_cstr.size(), - opts.license, [](auto res) { auto callback_ptr = callback::get_async_callback( diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index e9e914d6b..974697d53 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2282,7 +2282,6 @@ convert(const lb_owned_topic_t* item) item->views_count, item->shares_count, item->topic_type, - item->license, item->detail_url, item->created_at, item->updated_at }; diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index b955ca854..eb8008ffa 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -61,7 +61,7 @@ public static native void oauthBuild(String clientId, int callbackPort, public static native void freeContentContext(long context); - public static native void contentContextTopicsMine(long context, Object opts, AsyncCallback callback); + public static native void contentContextMyTopics(long context, Object opts, AsyncCallback callback); public static native void contentContextCreateTopic(long context, Object opts, AsyncCallback callback); diff --git a/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java b/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java index 4a002d1a6..007bd826c 100644 --- a/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java +++ b/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java @@ -34,10 +34,10 @@ public void close() throws Exception { * @return A Future representing the result of the operation * @throws OpenApiException If an error occurs */ - public CompletableFuture getTopicsMine(ListMyTopicsOptions opts) + public CompletableFuture getMyTopics(MyTopicsOptions opts) throws OpenApiException { return AsyncCallback.executeTask((callback) -> { - SdkNative.contentContextTopicsMine(raw, opts, callback); + SdkNative.contentContextMyTopics(raw, opts, callback); }); } @@ -48,7 +48,7 @@ public CompletableFuture getTopicsMine(ListMyTopicsOptions opts) * @return A Future representing the result of the operation * @throws OpenApiException If an error occurs */ - public CompletableFuture createTopic(CreateTopicOptions opts) + public CompletableFuture createTopic(CreateTopicOptions opts) throws OpenApiException { return AsyncCallback.executeTask((callback) -> { SdkNative.contentContextCreateTopic(raw, opts, callback); diff --git a/java/javasrc/src/main/java/com/longbridge/content/CreateTopicOptions.java b/java/javasrc/src/main/java/com/longbridge/content/CreateTopicOptions.java index 21555771a..15b3da7c5 100644 --- a/java/javasrc/src/main/java/com/longbridge/content/CreateTopicOptions.java +++ b/java/javasrc/src/main/java/com/longbridge/content/CreateTopicOptions.java @@ -10,7 +10,6 @@ public class CreateTopicOptions { private String topicType; private String[] tickers; private String[] hashtags; - private Integer license; /** * Constructs a create-topic request. @@ -56,14 +55,4 @@ public CreateTopicOptions setHashtags(String[] hashtags) { return this; } - /** - * Sets the license: 0=none (default), 1=original, 2=non-original. - * - * @param license license value - * @return this instance for chaining - */ - public CreateTopicOptions setLicense(int license) { - this.license = license; - return this; - } } diff --git a/java/javasrc/src/main/java/com/longbridge/content/ListMyTopicsOptions.java b/java/javasrc/src/main/java/com/longbridge/content/MyTopicsOptions.java similarity index 81% rename from java/javasrc/src/main/java/com/longbridge/content/ListMyTopicsOptions.java rename to java/javasrc/src/main/java/com/longbridge/content/MyTopicsOptions.java index 4de859767..db8593caf 100644 --- a/java/javasrc/src/main/java/com/longbridge/content/ListMyTopicsOptions.java +++ b/java/javasrc/src/main/java/com/longbridge/content/MyTopicsOptions.java @@ -4,7 +4,7 @@ * Options for listing topics created by the current authenticated user */ @SuppressWarnings("unused") -public class ListMyTopicsOptions { +public class MyTopicsOptions { private Integer page; private Integer size; private String topicType; @@ -15,7 +15,7 @@ public class ListMyTopicsOptions { * @param page page number * @return this instance for chaining */ - public ListMyTopicsOptions setPage(int page) { + public MyTopicsOptions setPage(int page) { this.page = page; return this; } @@ -26,7 +26,7 @@ public ListMyTopicsOptions setPage(int page) { * @param size records per page * @return this instance for chaining */ - public ListMyTopicsOptions setSize(int size) { + public MyTopicsOptions setSize(int size) { this.size = size; return this; } @@ -37,7 +37,7 @@ public ListMyTopicsOptions setSize(int size) { * @param topicType topic type filter * @return this instance for chaining */ - public ListMyTopicsOptions setTopicType(String topicType) { + public MyTopicsOptions setTopicType(String topicType) { this.topicType = topicType; return this; } diff --git a/java/javasrc/src/main/java/com/longbridge/content/OwnedTopic.java b/java/javasrc/src/main/java/com/longbridge/content/OwnedTopic.java index 769164427..9956e5121 100644 --- a/java/javasrc/src/main/java/com/longbridge/content/OwnedTopic.java +++ b/java/javasrc/src/main/java/com/longbridge/content/OwnedTopic.java @@ -19,7 +19,6 @@ public class OwnedTopic { private int viewsCount; private int sharesCount; private String topicType; - private int license; private String detailUrl; private OffsetDateTime createdAt; private OffsetDateTime updatedAt; @@ -63,9 +62,6 @@ public class OwnedTopic { /** Returns the content type: "article" or "post". */ public String getTopicType() { return topicType; } - /** Returns the license: 0=none, 1=original, 2=non-original. */ - public int getLicense() { return license; } - /** Returns the URL to the full topic page. */ public String getDetailUrl() { return detailUrl; } diff --git a/java/src/content_context.rs b/java/src/content_context.rs index f5aade65a..c2918a313 100644 --- a/java/src/content_context.rs +++ b/java/src/content_context.rs @@ -6,7 +6,7 @@ use jni::{ }; use longbridge::{ Config, - content::{ContentContext, CreateTopicOptions, ListMyTopicsOptions}, + content::{ContentContext, CreateTopicOptions, MyTopicsOptions}, }; use crate::{ @@ -42,7 +42,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_freeContentContext( } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextTopicsMine( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextMyTopics( mut env: JNIEnv, _class: JClass, context: i64, @@ -58,7 +58,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextTopics Ok(ObjectArray( context .ctx - .topics_mine(ListMyTopicsOptions { + .my_topics(MyTopicsOptions { page: page.map(i32::from), size: size.map(i32::from), topic_type, @@ -85,7 +85,6 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextCreate let topic_type: Option = get_field(env, &opts, "topicType")?; let tickers: Option> = get_field(env, &opts, "tickers")?; let hashtags: Option> = get_field(env, &opts, "hashtags")?; - let license: Option = get_field(env, &opts, "license")?; async_util::execute(env, callback, async move { Ok(context .ctx @@ -95,7 +94,6 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextCreate topic_type, tickers: tickers.map(|a| a.0), hashtags: hashtags.map(|a| a.0), - license: license.map(i32::from), }) .await?) })?; diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index 8f8551b69..002384ecd 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -1042,7 +1042,6 @@ impl_java_class!( views_count, shares_count, topic_type, - license, detail_url, created_at, updated_at diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 6be0948b7..0dfc97bee 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -222,9 +222,9 @@ export declare class ContentContext { /** Create a new `ContentContext` */ static new(config: Config): ContentContext /** Get topics created by the current authenticated user */ - topicsMine(req?: ListMyTopicsRequest | undefined | null): Promise> + myTopics(req?: MyTopicsRequest | undefined | null): Promise> /** Create a new topic */ - createTopic(req: CreateTopicRequest): Promise + createTopic(req: CreateTopicRequest): Promise /** Get discussion topics list */ topics(symbol: string): Promise> /** Get news list */ @@ -985,8 +985,6 @@ export declare class OwnedTopic { get sharesCount(): number /** Content type: "article" or "post" */ get topicType(): string - /** License: 0=none, 1=original, 2=non-original */ - get license(): number /** URL to the full topic page */ get detailUrl(): string /** Created time */ @@ -2765,8 +2763,6 @@ export interface CreateTopicRequest { tickers?: Array /** Hashtag names, max 5 */ hashtags?: Array - /** License: 0=none (default), 1=original, 2=non-original */ - license?: number } /** An request to create a watchlist group */ @@ -2955,16 +2951,6 @@ export declare const enum Language { EN = 2 } -/** Options for listing topics created by the current authenticated user */ -export interface ListMyTopicsRequest { - /** Page number (default 1) */ - page?: number - /** Records per page, range 1~500 (default 50) */ - size?: number - /** Filter by topic type: "article" or "post"; empty returns all */ - topicType?: string -} - export declare const enum Market { /** Unknown */ Unknown = 0, @@ -2980,6 +2966,16 @@ export declare const enum Market { Crypto = 5 } +/** Options for listing topics created by the current authenticated user */ +export interface MyTopicsRequest { + /** Page number (default 1) */ + page?: number + /** Records per page, range 1~500 (default 50) */ + size?: number + /** Filter by topic type: "article" or "post"; empty returns all */ + topicType?: string +} + /** Option direction */ export declare const enum OptionDirection { /** Unknown */ diff --git a/nodejs/src/content/context.rs b/nodejs/src/content/context.rs index 15ade5650..275641dfc 100644 --- a/nodejs/src/content/context.rs +++ b/nodejs/src/content/context.rs @@ -5,7 +5,7 @@ use napi::Result; use crate::{ config::Config, content::{ - requests::{CreateTopicRequest, ListMyTopicsRequest}, + requests::{CreateTopicRequest, MyTopicsRequest}, types::{NewsItem, OwnedTopic, TopicItem}, }, error::ErrorNewType, @@ -30,9 +30,9 @@ impl ContentContext { /// Get topics created by the current authenticated user #[napi] - pub async fn topics_mine(&self, req: Option) -> Result> { + pub async fn my_topics(&self, req: Option) -> Result> { self.ctx - .topics_mine(req.unwrap_or_default().into()) + .my_topics(req.unwrap_or_default().into()) .await .map_err(ErrorNewType)? .into_iter() diff --git a/nodejs/src/content/requests.rs b/nodejs/src/content/requests.rs index d609a0d57..74d26670e 100644 --- a/nodejs/src/content/requests.rs +++ b/nodejs/src/content/requests.rs @@ -1,9 +1,9 @@ -use longbridge::content::{CreateTopicOptions, ListMyTopicsOptions}; +use longbridge::content::{CreateTopicOptions, MyTopicsOptions}; /// Options for listing topics created by the current authenticated user #[napi_derive::napi(object)] #[derive(Debug, Default)] -pub struct ListMyTopicsRequest { +pub struct MyTopicsRequest { /// Page number (default 1) pub page: Option, /// Records per page, range 1~500 (default 50) @@ -12,13 +12,13 @@ pub struct ListMyTopicsRequest { pub topic_type: Option, } -impl From for ListMyTopicsOptions { +impl From for MyTopicsOptions { fn from( - ListMyTopicsRequest { + MyTopicsRequest { page, size, topic_type, - }: ListMyTopicsRequest, + }: MyTopicsRequest, ) -> Self { Self { page, @@ -42,8 +42,6 @@ pub struct CreateTopicRequest { pub tickers: Option>, /// Hashtag names, max 5 pub hashtags: Option>, - /// License: 0=none (default), 1=original, 2=non-original - pub license: Option, } impl From for CreateTopicOptions { @@ -54,7 +52,6 @@ impl From for CreateTopicOptions { topic_type, tickers, hashtags, - license, }: CreateTopicRequest, ) -> Self { Self { @@ -63,7 +60,6 @@ impl From for CreateTopicOptions { topic_type, tickers, hashtags, - license, } } } diff --git a/nodejs/src/content/types.rs b/nodejs/src/content/types.rs index cc91c46d6..ea24ee420 100644 --- a/nodejs/src/content/types.rs +++ b/nodejs/src/content/types.rs @@ -61,8 +61,6 @@ pub struct OwnedTopic { shares_count: i32, /// Content type: "article" or "post" topic_type: String, - /// License: 0=none, 1=original, 2=non-original - license: i32, /// URL to the full topic page detail_url: String, /// Created time diff --git a/python/src/content/context.rs b/python/src/content/context.rs index 1acf4b0fd..579016364 100644 --- a/python/src/content/context.rs +++ b/python/src/content/context.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use longbridge::{ blocking::ContentContextSync, - content::{CreateTopicOptions, ListMyTopicsOptions}, + content::{CreateTopicOptions, MyTopicsOptions}, }; use pyo3::prelude::*; @@ -28,14 +28,14 @@ impl ContentContext { /// Get topics created by the current authenticated user #[pyo3(signature = (page = None, size = None, topic_type = None))] - pub fn topics_mine( + pub fn my_topics( &self, page: Option, size: Option, topic_type: Option, ) -> PyResult> { self.ctx - .topics_mine(ListMyTopicsOptions { + .my_topics(MyTopicsOptions { page, size, topic_type, @@ -47,7 +47,7 @@ impl ContentContext { } /// Create a new topic - #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None, license = None))] + #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] pub fn create_topic( &self, title: String, @@ -55,7 +55,6 @@ impl ContentContext { topic_type: Option, tickers: Option>, hashtags: Option>, - license: Option, ) -> PyResult { Ok(self .ctx @@ -65,7 +64,6 @@ impl ContentContext { topic_type, tickers, hashtags, - license, }) .map_err(ErrorNewType)?) } diff --git a/python/src/content/context_async.rs b/python/src/content/context_async.rs index bcd641316..ca390a449 100644 --- a/python/src/content/context_async.rs +++ b/python/src/content/context_async.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge::content::{ContentContext, CreateTopicOptions, ListMyTopicsOptions}; +use longbridge::content::{ContentContext, CreateTopicOptions, MyTopicsOptions}; use pyo3::{prelude::*, types::PyType}; use crate::{ @@ -27,7 +27,7 @@ impl AsyncContentContext { /// Get topics created by the current authenticated user. Returns awaitable. #[pyo3(signature = (page = None, size = None, topic_type = None))] - fn topics_mine( + fn my_topics( &self, py: Python<'_>, page: Option, @@ -37,7 +37,7 @@ impl AsyncContentContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let v = ctx - .topics_mine(ListMyTopicsOptions { + .my_topics(MyTopicsOptions { page, size, topic_type, @@ -52,7 +52,7 @@ impl AsyncContentContext { } /// Create a new topic. Returns awaitable. - #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None, license = None))] + #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] fn create_topic( &self, py: Python<'_>, @@ -61,7 +61,6 @@ impl AsyncContentContext { topic_type: Option, tickers: Option>, hashtags: Option>, - license: Option, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { @@ -72,7 +71,6 @@ impl AsyncContentContext { topic_type, tickers, hashtags, - license, }) .await .map_err(ErrorNewType)?) diff --git a/python/src/content/types.rs b/python/src/content/types.rs index 3bcd19412..bf185ceee 100644 --- a/python/src/content/types.rs +++ b/python/src/content/types.rs @@ -63,8 +63,6 @@ pub(crate) struct OwnedTopic { shares_count: i32, /// Content type: "article" or "post" topic_type: String, - /// License: 0=none, 1=original, 2=non-original - license: i32, /// URL to the full topic page detail_url: String, /// Created time diff --git a/rust/src/blocking/content.rs b/rust/src/blocking/content.rs index 4b20fd332..fedf63570 100644 --- a/rust/src/blocking/content.rs +++ b/rust/src/blocking/content.rs @@ -6,7 +6,7 @@ use crate::{ Config, Result, blocking::runtime::BlockingRuntime, content::{ - ContentContext, CreateTopicOptions, ListMyTopicsOptions, NewsItem, OwnedTopic, TopicItem, + ContentContext, CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem, }, }; @@ -31,9 +31,9 @@ impl ContentContextSync { } /// Get topics created by the current authenticated user - pub fn topics_mine(&self, opts: ListMyTopicsOptions) -> Result> { + pub fn my_topics(&self, opts: MyTopicsOptions) -> Result> { self.rt - .call(move |ctx| async move { ctx.topics_mine(opts).await }) + .call(move |ctx| async move { ctx.my_topics(opts).await }) } /// Create a new topic diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs index d48bb1daf..abc71c9d3 100644 --- a/rust/src/content/context.rs +++ b/rust/src/content/context.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use longbridge_httpcli::{HttpClient, Json, Method}; use serde::Deserialize; -use super::types::{CreateTopicOptions, ListMyTopicsOptions, NewsItem, OwnedTopic, TopicItem}; +use super::types::{CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem}; use crate::{Config, Result}; struct InnerContentContext { @@ -25,7 +25,7 @@ impl ContentContext { /// Get topics created by the current authenticated user /// /// Path: GET /v1/content/topics/mine - pub async fn topics_mine(&self, opts: ListMyTopicsOptions) -> Result> { + pub async fn my_topics(&self, opts: MyTopicsOptions) -> Result> { #[derive(Debug, Deserialize)] struct Response { items: Vec, diff --git a/rust/src/content/mod.rs b/rust/src/content/mod.rs index 829b13428..fb0ccbc23 100644 --- a/rust/src/content/mod.rs +++ b/rust/src/content/mod.rs @@ -5,6 +5,6 @@ mod types; pub use context::ContentContext; pub use types::{ - CreateTopicOptions, ListMyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, + CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, TopicItem, }; diff --git a/rust/src/content/types.rs b/rust/src/content/types.rs index 9562dd287..0e8e5187a 100644 --- a/rust/src/content/types.rs +++ b/rust/src/content/types.rs @@ -57,18 +57,20 @@ pub struct OwnedTopic { #[serde(default)] pub images: Vec, /// Likes count + #[serde(default)] pub likes_count: i32, /// Comments count + #[serde(default)] pub comments_count: i32, /// Views count + #[serde(default)] pub views_count: i32, /// Shares count + #[serde(default)] pub shares_count: i32, /// Content type: "article" or "post" #[serde(default)] pub topic_type: String, - /// License: 0=none, 1=original, 2=non-original - pub license: i32, /// URL to the full topic page #[serde(default)] pub detail_url: String, @@ -88,7 +90,7 @@ pub struct OwnedTopic { /// Options for listing topics created by the current authenticated user #[derive(Debug, Default, Clone, Serialize)] -pub struct ListMyTopicsOptions { +pub struct MyTopicsOptions { /// Page number (default 1) #[serde(skip_serializing_if = "Option::is_none")] pub page: Option, @@ -116,9 +118,6 @@ pub struct CreateTopicOptions { /// Hashtag names, max 5 #[serde(skip_serializing_if = "Option::is_none")] pub hashtags: Option>, - /// License: 0=none (default), 1=original, 2=non-original - #[serde(skip_serializing_if = "Option::is_none")] - pub license: Option, } /// Topic item From 44adf43102abf76b3f1ac2ba0507d9336e2fb23f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 16:18:51 +0800 Subject: [PATCH 2/8] . --- c/csrc/include/longbridge.h | 12 ++++++------ rust/src/content/mod.rs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index cb09f4795..a027a0a48 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -4251,12 +4251,12 @@ void lb_content_context_release(const struct lb_content_context_t *ctx); * @param callback Async callback * @param userdata User data passed to the callback */ -void lb_content_context_topics_mine(const struct lb_content_context_t *ctx, - int32_t page, - int32_t size, - const char *topic_type, - lb_async_callback_t callback, - void *userdata); +void lb_content_context_my_topics(const struct lb_content_context_t *ctx, + int32_t page, + int32_t size, + const char *topic_type, + lb_async_callback_t callback, + void *userdata); /** * Create a new topic diff --git a/rust/src/content/mod.rs b/rust/src/content/mod.rs index fb0ccbc23..bad07fbdd 100644 --- a/rust/src/content/mod.rs +++ b/rust/src/content/mod.rs @@ -5,6 +5,5 @@ mod types; pub use context::ContentContext; pub use types::{ - CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, - TopicItem, + CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, TopicItem, }; From 2a881b778b5d360f881d99e33899011a1bfe7e22 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:08:28 +0800 Subject: [PATCH 3/8] feat(content): add topic_detail, list_topic_replies, create_topic_reply - types: add ListTopicRepliesOptions, CreateReplyOptions, TopicReply (with author, images, counts, created_at) - context: add async topic_detail, list_topic_replies, create_topic_reply methods - blocking: add sync wrappers for the three new methods - python: add TopicReply pyclass, sync + async bindings for all three methods - doc comments: update create_topic and create_topic_reply with permission, tickers auto-link warning, rate-limit details Co-Authored-By: Claude Sonnet 4.6 --- python/src/content/context.rs | 64 ++++++++++++++++- python/src/content/context_async.rs | 83 ++++++++++++++++++++- python/src/content/mod.rs | 1 + python/src/content/types.rs | 26 +++++++ rust/src/blocking/content.rs | 32 ++++++++- rust/src/content/context.rs | 108 +++++++++++++++++++++++++++- rust/src/content/mod.rs | 3 +- rust/src/content/types.rs | 58 +++++++++++++++ 8 files changed, 365 insertions(+), 10 deletions(-) diff --git a/python/src/content/context.rs b/python/src/content/context.rs index 579016364..55cc4adcb 100644 --- a/python/src/content/context.rs +++ b/python/src/content/context.rs @@ -2,13 +2,13 @@ use std::sync::Arc; use longbridge::{ blocking::ContentContextSync, - content::{CreateTopicOptions, MyTopicsOptions}, + content::{CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions}, }; use pyo3::prelude::*; use crate::{ config::Config, - content::types::{NewsItem, OwnedTopic, TopicItem}, + content::types::{NewsItem, OwnedTopic, TopicItem, TopicReply}, error::ErrorNewType, }; @@ -46,7 +46,19 @@ impl ContentContext { .collect() } - /// Create a new topic + /// Create a new community topic. + /// + /// Two content types are supported: + /// - `post` (default): plain text only; Markdown is NOT rendered. + /// - `article`: Markdown body; `title` is required. + /// + /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` + /// to associate additional symbols not mentioned in the body. + /// WARNING: do not abuse symbol linking for unrelated stocks. + /// + /// Rate limit: max 3 topics/min and 10/24h per user (raises 429 on excess). #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] pub fn create_topic( &self, @@ -87,4 +99,50 @@ impl ContentContext { .map(TryInto::try_into) .collect() } + + /// Get full details of a topic by its ID + pub fn topic_detail(&self, id: String) -> PyResult { + self.ctx.topic_detail(id).map_err(ErrorNewType)?.try_into() + } + + /// List replies on a topic + #[pyo3(signature = (topic_id, page = None, size = None))] + pub fn list_topic_replies( + &self, + topic_id: String, + page: Option, + size: Option, + ) -> PyResult> { + self.ctx + .list_topic_replies(topic_id, ListTopicRepliesOptions { page, size }) + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + /// Post a reply to a community topic. + /// + /// Body is plain text only — Markdown is not rendered. + /// + /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. + /// WARNING: do not abuse symbol linking for unrelated stocks. + /// + /// Rate limit per user per topic: first 3 replies have no wait; subsequent replies + /// require incrementally longer intervals (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). + /// Raises 429 on excess. + #[pyo3(signature = (topic_id, body, reply_to_id = None))] + pub fn create_topic_reply( + &self, + topic_id: String, + body: String, + reply_to_id: Option, + ) -> PyResult { + self.ctx + .create_topic_reply(topic_id, CreateReplyOptions { body, reply_to_id }) + .map_err(ErrorNewType)? + .try_into() + } } diff --git a/python/src/content/context_async.rs b/python/src/content/context_async.rs index ca390a449..dd5883777 100644 --- a/python/src/content/context_async.rs +++ b/python/src/content/context_async.rs @@ -1,11 +1,13 @@ use std::sync::Arc; -use longbridge::content::{ContentContext, CreateTopicOptions, MyTopicsOptions}; +use longbridge::content::{ + ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, +}; use pyo3::{prelude::*, types::PyType}; use crate::{ config::Config, - content::types::{NewsItem, OwnedTopic, TopicItem}, + content::types::{NewsItem, OwnedTopic, TopicItem, TopicReply}, error::ErrorNewType, }; @@ -51,7 +53,19 @@ impl AsyncContentContext { .map(|b| b.unbind()) } - /// Create a new topic. Returns awaitable. + /// Create a new community topic. Returns awaitable. + /// + /// Two content types are supported: + /// - `post` (default): plain text only; Markdown is NOT rendered. + /// - `article`: Markdown body; `title` is required. + /// + /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` + /// to associate additional symbols not mentioned in the body. + /// WARNING: do not abuse symbol linking for unrelated stocks. + /// + /// Rate limit: max 3 topics/min and 10/24h per user (raises 429 on excess). #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] fn create_topic( &self, @@ -101,4 +115,67 @@ impl AsyncContentContext { }) .map(|b| b.unbind()) } + + /// Get full details of a topic by its ID. Returns awaitable. + fn topic_detail(&self, py: Python<'_>, id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.topic_detail(id).await.map_err(ErrorNewType)?; + OwnedTopic::try_from(v) + }) + .map(|b| b.unbind()) + } + + /// List replies on a topic. Returns awaitable. + #[pyo3(signature = (topic_id, page = None, size = None))] + fn list_topic_replies( + &self, + py: Python<'_>, + topic_id: String, + page: Option, + size: Option, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx + .list_topic_replies(topic_id, ListTopicRepliesOptions { page, size }) + .await + .map_err(ErrorNewType)?; + v.into_iter() + .map(|x| -> PyResult { x.try_into() }) + .collect::>>() + }) + .map(|b| b.unbind()) + } + + /// Post a reply to a community topic. Returns awaitable. + /// + /// Body is plain text only — Markdown is not rendered. + /// + /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. + /// WARNING: do not abuse symbol linking for unrelated stocks. + /// + /// Rate limit per user per topic: first 3 replies have no wait; subsequent replies + /// require incrementally longer intervals (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). + /// Raises 429 on excess. + #[pyo3(signature = (topic_id, body, reply_to_id = None))] + fn create_topic_reply( + &self, + py: Python<'_>, + topic_id: String, + body: String, + reply_to_id: Option, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx + .create_topic_reply(topic_id, CreateReplyOptions { body, reply_to_id }) + .await + .map_err(ErrorNewType)?; + TopicReply::try_from(v) + }) + .map(|b| b.unbind()) + } } diff --git a/python/src/content/mod.rs b/python/src/content/mod.rs index 7c8b0c8f9..59aec4049 100644 --- a/python/src/content/mod.rs +++ b/python/src/content/mod.rs @@ -10,6 +10,7 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; Ok(()) diff --git a/python/src/content/types.rs b/python/src/content/types.rs index bf185ceee..a91853744 100644 --- a/python/src/content/types.rs +++ b/python/src/content/types.rs @@ -94,6 +94,32 @@ pub(crate) struct TopicItem { shares_count: i32, } +/// A reply on a topic +#[pyclass(skip_from_py_object)] +#[derive(Debug, PyObject, Clone)] +#[py(remote = "longbridge::content::TopicReply")] +pub(crate) struct TopicReply { + /// Reply ID + id: String, + /// Topic ID this reply belongs to + topic_id: String, + /// Reply body (plain text) + body: String, + /// ID of the parent reply ("0" means top-level) + reply_to_id: String, + /// Author info + author: TopicAuthor, + /// Attached images + #[py(array)] + images: Vec, + /// Likes count + likes_count: i32, + /// Nested replies count + comments_count: i32, + /// Created time + created_at: PyOffsetDateTimeWrapper, +} + /// News item #[pyclass(skip_from_py_object)] #[derive(Debug, PyObject, Clone)] diff --git a/rust/src/blocking/content.rs b/rust/src/blocking/content.rs index fedf63570..0baf9ccd0 100644 --- a/rust/src/blocking/content.rs +++ b/rust/src/blocking/content.rs @@ -6,7 +6,8 @@ use crate::{ Config, Result, blocking::runtime::BlockingRuntime, content::{ - ContentContext, CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem, + ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, + MyTopicsOptions, NewsItem, OwnedTopic, TopicItem, TopicReply, }, }; @@ -55,4 +56,33 @@ impl ContentContextSync { self.rt .call(move |ctx| async move { ctx.news(symbol).await }) } + + /// Get full details of a topic by its ID + pub fn topic_detail(&self, id: impl Into) -> Result { + let id = id.into(); + self.rt + .call(move |ctx| async move { ctx.topic_detail(id).await }) + } + + /// List replies on a topic + pub fn list_topic_replies( + &self, + topic_id: impl Into, + opts: ListTopicRepliesOptions, + ) -> Result> { + let topic_id = topic_id.into(); + self.rt + .call(move |ctx| async move { ctx.list_topic_replies(topic_id, opts).await }) + } + + /// Post a reply to a topic + pub fn create_topic_reply( + &self, + topic_id: impl Into, + opts: CreateReplyOptions, + ) -> Result { + let topic_id = topic_id.into(); + self.rt + .call(move |ctx| async move { ctx.create_topic_reply(topic_id, opts).await }) + } } diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs index abc71c9d3..5592d1431 100644 --- a/rust/src/content/context.rs +++ b/rust/src/content/context.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use longbridge_httpcli::{HttpClient, Json, Method}; use serde::Deserialize; -use super::types::{CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicItem}; +use super::types::{ + CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, NewsItem, + OwnedTopic, TopicItem, TopicReply, +}; use crate::{Config, Result}; struct InnerContentContext { @@ -43,9 +46,22 @@ impl ContentContext { .items) } - /// Create a new topic + /// Create a new community topic. /// /// Path: POST /v1/content/topics + /// + /// Two content types are supported: + /// - `post` (default): plain text only; Markdown is NOT rendered. + /// - `article`: Markdown body (server converts to HTML); `title` is required. + /// + /// Permission: user must hold a funded Longbridge account (returns 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` + /// to associate additional symbols not mentioned in the body. + /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict + /// publishing or suspend the account. + /// + /// Rate limit: max 3 topics/min and 10/24h per user (429 on excess). pub async fn create_topic(&self, opts: CreateTopicOptions) -> Result { #[derive(Debug, Deserialize)] struct TopicId { @@ -89,6 +105,94 @@ impl ContentContext { .items) } + /// Get full details of a topic by its ID + /// + /// Path: GET /v1/content/topics/{id} + pub async fn topic_detail(&self, id: impl Into) -> Result { + #[derive(Debug, Deserialize)] + struct Response { + item: OwnedTopic, + } + + let id = id.into(); + Ok(self + .0 + .http_cli + .request(Method::GET, format!("/v1/content/topics/{id}")) + .response::>() + .send() + .await? + .0 + .item) + } + + /// List replies on a topic + /// + /// Path: GET /v1/content/topics/{topic_id}/comments + pub async fn list_topic_replies( + &self, + topic_id: impl Into, + opts: ListTopicRepliesOptions, + ) -> Result> { + #[derive(Debug, Deserialize)] + struct Response { + items: Vec, + } + + let topic_id = topic_id.into(); + Ok(self + .0 + .http_cli + .request(Method::GET, format!("/v1/content/topics/{topic_id}/comments")) + .query_params(opts) + .response::>() + .send() + .await? + .0 + .items) + } + + /// Post a reply to a community topic. + /// + /// Path: POST /v1/content/topics/{topic_id}/comments + /// + /// Body is plain text only — Markdown is not rendered. + /// + /// Permission: user must hold a funded Longbridge account (returns 403 otherwise). + /// + /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. + /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict + /// publishing or suspend the account. + /// + /// Rate limit per user per topic: first 3 replies have no interval; subsequent replies + /// must wait an incrementally longer interval (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). + /// Returns 429 on excess. + pub async fn create_topic_reply( + &self, + topic_id: impl Into, + opts: CreateReplyOptions, + ) -> Result { + #[derive(Debug, Deserialize)] + struct Response { + item: TopicReply, + } + + let topic_id = topic_id.into(); + Ok(self + .0 + .http_cli + .request( + Method::POST, + format!("/v1/content/topics/{topic_id}/comments"), + ) + .body(Json(opts)) + .response::>() + .send() + .await? + .0 + .item) + } + /// Get news list pub async fn news(&self, symbol: impl Into) -> Result> { #[derive(Debug, Deserialize)] diff --git a/rust/src/content/mod.rs b/rust/src/content/mod.rs index bad07fbdd..aa038349d 100644 --- a/rust/src/content/mod.rs +++ b/rust/src/content/mod.rs @@ -5,5 +5,6 @@ mod types; pub use context::ContentContext; pub use types::{ - CreateTopicOptions, MyTopicsOptions, NewsItem, OwnedTopic, TopicAuthor, TopicImage, TopicItem, + CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, NewsItem, + OwnedTopic, TopicAuthor, TopicImage, TopicItem, TopicReply, }; diff --git a/rust/src/content/types.rs b/rust/src/content/types.rs index 0e8e5187a..f34f77fd9 100644 --- a/rust/src/content/types.rs +++ b/rust/src/content/types.rs @@ -147,6 +147,64 @@ pub struct TopicItem { pub shares_count: i32, } +/// Options for listing replies on a topic +#[derive(Debug, Default, Clone, Serialize)] +pub struct ListTopicRepliesOptions { + /// Page number (default 1) + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + /// Records per page, range 1~50 (default 20) + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, +} + +/// Options for posting a reply to a topic +#[derive(Debug, Clone, Serialize)] +pub struct CreateReplyOptions { + /// Reply body. Plain text only — Markdown is not rendered. + /// + /// Stock symbols mentioned in the body (e.g. `700.HK`, `TSLA.US`) are + /// automatically recognized and linked as related stocks by the platform. + /// Use `tickers` in the parent topic to associate additional stocks not + /// mentioned in the body. + pub body: String, + /// ID of the reply to reply to. Set to `"0"` or omit to post a top-level reply. + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to_id: Option, +} + +/// A reply on a topic +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopicReply { + /// Reply ID + pub id: String, + /// Topic ID this reply belongs to + pub topic_id: String, + /// Reply body (plain text) + #[serde(default)] + pub body: String, + /// ID of the parent reply (`"0"` means top-level) + #[serde(default)] + pub reply_to_id: String, + /// Author info + pub author: TopicAuthor, + /// Attached images + #[serde(default)] + pub images: Vec, + /// Likes count + #[serde(default)] + pub likes_count: i32, + /// Nested replies count + #[serde(default)] + pub comments_count: i32, + /// Created time + #[serde( + serialize_with = "time::serde::rfc3339::serialize", + deserialize_with = "serde_utils::timestamp::deserialize" + )] + pub created_at: OffsetDateTime, +} + /// News item #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewsItem { From 5c6ebc3a5b60605769a61177417e83d15dcd329b Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:21:00 +0800 Subject: [PATCH 4/8] fix: correct warning wording from account suspension to muting Co-Authored-By: Claude Sonnet 4.6 --- rust/src/content/context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs index 5592d1431..915f793e7 100644 --- a/rust/src/content/context.rs +++ b/rust/src/content/context.rs @@ -59,7 +59,7 @@ impl ContentContext { /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` /// to associate additional symbols not mentioned in the body. /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict - /// publishing or suspend the account. + /// publishing or mute the account. /// /// Rate limit: max 3 topics/min and 10/24h per user (429 on excess). pub async fn create_topic(&self, opts: CreateTopicOptions) -> Result { @@ -162,7 +162,7 @@ impl ContentContext { /// /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict - /// publishing or suspend the account. + /// publishing or mute the account. /// /// Rate limit per user per topic: first 3 replies have no interval; subsequent replies /// must wait an incrementally longer interval (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). From 70b2ae44b9cdfcdf41693bc503716173e989e03b Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:23:53 +0800 Subject: [PATCH 5/8] refactor: simplify SDK doc comments to reference docs URL Co-Authored-By: Claude Sonnet 4.6 --- python/src/content/context.rs | 23 ++-------------- python/src/content/context_async.rs | 23 ++-------------- rust/src/content/context.rs | 41 ++++++----------------------- 3 files changed, 12 insertions(+), 75 deletions(-) diff --git a/python/src/content/context.rs b/python/src/content/context.rs index 55cc4adcb..6c9b37f5d 100644 --- a/python/src/content/context.rs +++ b/python/src/content/context.rs @@ -48,17 +48,7 @@ impl ContentContext { /// Create a new community topic. /// - /// Two content types are supported: - /// - `post` (default): plain text only; Markdown is NOT rendered. - /// - `article`: Markdown body; `title` is required. - /// - /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` - /// to associate additional symbols not mentioned in the body. - /// WARNING: do not abuse symbol linking for unrelated stocks. - /// - /// Rate limit: max 3 topics/min and 10/24h per user (raises 429 on excess). + /// See: #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] pub fn create_topic( &self, @@ -123,16 +113,7 @@ impl ContentContext { /// Post a reply to a community topic. /// - /// Body is plain text only — Markdown is not rendered. - /// - /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. - /// WARNING: do not abuse symbol linking for unrelated stocks. - /// - /// Rate limit per user per topic: first 3 replies have no wait; subsequent replies - /// require incrementally longer intervals (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). - /// Raises 429 on excess. + /// See: #[pyo3(signature = (topic_id, body, reply_to_id = None))] pub fn create_topic_reply( &self, diff --git a/python/src/content/context_async.rs b/python/src/content/context_async.rs index dd5883777..f04291337 100644 --- a/python/src/content/context_async.rs +++ b/python/src/content/context_async.rs @@ -55,17 +55,7 @@ impl AsyncContentContext { /// Create a new community topic. Returns awaitable. /// - /// Two content types are supported: - /// - `post` (default): plain text only; Markdown is NOT rendered. - /// - `article`: Markdown body; `title` is required. - /// - /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` - /// to associate additional symbols not mentioned in the body. - /// WARNING: do not abuse symbol linking for unrelated stocks. - /// - /// Rate limit: max 3 topics/min and 10/24h per user (raises 429 on excess). + /// See: #[pyo3(signature = (title, body, topic_type = None, tickers = None, hashtags = None))] fn create_topic( &self, @@ -150,16 +140,7 @@ impl AsyncContentContext { /// Post a reply to a community topic. Returns awaitable. /// - /// Body is plain text only — Markdown is not rendered. - /// - /// Permission: user must hold a funded Longbridge account (raises 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. - /// WARNING: do not abuse symbol linking for unrelated stocks. - /// - /// Rate limit per user per topic: first 3 replies have no wait; subsequent replies - /// require incrementally longer intervals (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). - /// Raises 429 on excess. + /// See: #[pyo3(signature = (topic_id, body, reply_to_id = None))] fn create_topic_reply( &self, diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs index 915f793e7..a7725db8b 100644 --- a/rust/src/content/context.rs +++ b/rust/src/content/context.rs @@ -25,9 +25,9 @@ impl ContentContext { })) } - /// Get topics created by the current authenticated user + /// Get topics created by the current authenticated user. /// - /// Path: GET /v1/content/topics/mine + /// See: pub async fn my_topics(&self, opts: MyTopicsOptions) -> Result> { #[derive(Debug, Deserialize)] struct Response { @@ -48,20 +48,7 @@ impl ContentContext { /// Create a new community topic. /// - /// Path: POST /v1/content/topics - /// - /// Two content types are supported: - /// - `post` (default): plain text only; Markdown is NOT rendered. - /// - `article`: Markdown body (server converts to HTML); `title` is required. - /// - /// Permission: user must hold a funded Longbridge account (returns 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. Use `tickers` - /// to associate additional symbols not mentioned in the body. - /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict - /// publishing or mute the account. - /// - /// Rate limit: max 3 topics/min and 10/24h per user (429 on excess). + /// See: pub async fn create_topic(&self, opts: CreateTopicOptions) -> Result { #[derive(Debug, Deserialize)] struct TopicId { @@ -105,9 +92,9 @@ impl ContentContext { .items) } - /// Get full details of a topic by its ID + /// Get full details of a topic by its ID. /// - /// Path: GET /v1/content/topics/{id} + /// See: pub async fn topic_detail(&self, id: impl Into) -> Result { #[derive(Debug, Deserialize)] struct Response { @@ -126,9 +113,9 @@ impl ContentContext { .item) } - /// List replies on a topic + /// List replies on a topic. /// - /// Path: GET /v1/content/topics/{topic_id}/comments + /// See: pub async fn list_topic_replies( &self, topic_id: impl Into, @@ -154,19 +141,7 @@ impl ContentContext { /// Post a reply to a community topic. /// - /// Path: POST /v1/content/topics/{topic_id}/comments - /// - /// Body is plain text only — Markdown is not rendered. - /// - /// Permission: user must hold a funded Longbridge account (returns 403 otherwise). - /// - /// Symbols in body (e.g. "700.HK", "TSLA.US") are automatically linked. - /// WARNING: do not abuse symbol linking for unrelated stocks — moderation may restrict - /// publishing or mute the account. - /// - /// Rate limit per user per topic: first 3 replies have no interval; subsequent replies - /// must wait an incrementally longer interval (3 s → 5 s → 8 s → 13 s → 21 s → 34 s → 55 s cap). - /// Returns 429 on excess. + /// See: pub async fn create_topic_reply( &self, topic_id: impl Into, From 9e7f47e5143daa7e5ca7fe759e0c64df75ccc881 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:36:06 +0800 Subject: [PATCH 6/8] fmt --- python/src/content/context_async.rs | 3 ++- rust/src/content/context.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/python/src/content/context_async.rs b/python/src/content/context_async.rs index f04291337..f24b51576 100644 --- a/python/src/content/context_async.rs +++ b/python/src/content/context_async.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use longbridge::content::{ - ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, MyTopicsOptions, + ContentContext, CreateReplyOptions, CreateTopicOptions, ListTopicRepliesOptions, + MyTopicsOptions, }; use pyo3::{prelude::*, types::PyType}; diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs index a7725db8b..092eea9f2 100644 --- a/rust/src/content/context.rs +++ b/rust/src/content/context.rs @@ -130,7 +130,10 @@ impl ContentContext { Ok(self .0 .http_cli - .request(Method::GET, format!("/v1/content/topics/{topic_id}/comments")) + .request( + Method::GET, + format!("/v1/content/topics/{topic_id}/comments"), + ) .query_params(opts) .response::>() .send() From 0f366979930f1cab77d60ed625849d080f300bbf Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:39:52 +0800 Subject: [PATCH 7/8] . --- rust/src/content/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/content/types.rs b/rust/src/content/types.rs index f34f77fd9..cff8de7f6 100644 --- a/rust/src/content/types.rs +++ b/rust/src/content/types.rs @@ -168,7 +168,7 @@ pub struct CreateReplyOptions { /// Use `tickers` in the parent topic to associate additional stocks not /// mentioned in the body. pub body: String, - /// ID of the reply to reply to. Set to `"0"` or omit to post a top-level reply. + /// ID of the reply to. Set to `"0"` or omit to post a top-level reply. #[serde(skip_serializing_if = "Option::is_none")] pub reply_to_id: Option, } From bdeec6706ad1bb5e78cee0b5f693a34337ddf700 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 26 Mar 2026 18:40:15 +0800 Subject: [PATCH 8/8] . --- rust/src/content/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/content/types.rs b/rust/src/content/types.rs index cff8de7f6..3d3a04631 100644 --- a/rust/src/content/types.rs +++ b/rust/src/content/types.rs @@ -168,7 +168,7 @@ pub struct CreateReplyOptions { /// Use `tickers` in the parent topic to associate additional stocks not /// mentioned in the body. pub body: String, - /// ID of the reply to. Set to `"0"` or omit to post a top-level reply. + /// ID of the reply to. Set to `None` to post a top-level reply. #[serde(skip_serializing_if = "Option::is_none")] pub reply_to_id: Option, }