// Topics: create (POST /posts.json — Discourse creates topics by posting // the first post with a title), list per category (GET /c/<...>.json) and // latest (GET /latest.json), fetch one (GET /t/.json). package discourse import ( "context" "fmt" "strings" ) // TopicSummary is one row of a topic list. type TopicSummary struct { ID int `json:"id"` Title string `json:"title"` Slug string `json:"slug"` CategoryID int `json:"category_id"` PostsCount int `json:"posts_count"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Closed bool `json:"closed"` Archived bool `json:"archived"` } // Topic is the full topic payload (GET /t/.json); posts live under // PostStream.Posts. type Topic struct { ID int `json:"id"` Title string `json:"title"` Slug string `json:"slug"` CategoryID int `json:"category_id"` CreatedAt string `json:"created_at"` Closed bool `json:"closed"` Archived bool `json:"archived"` PostStream struct { Posts []Post `json:"posts"` } `json:"post_stream"` } // CreateTopicRequest creates a topic: title + markdown body + category. type CreateTopicRequest struct { Title string `json:"title"` Raw string `json:"raw"` Category int `json:"category"` Tags []string `json:"tags,omitempty"` } // CreateTopicResult is the POST /posts.json response for a topic // creation: the first post's id plus the new topic's id and slug. type CreateTopicResult struct { PostID int `json:"id"` TopicID int `json:"topic_id"` TopicSlug string `json:"topic_slug"` } // URL renders the topic's canonical URL on this instance. func (r CreateTopicResult) URL(baseURL string) string { if r.TopicID == 0 { return "" } slug := r.TopicSlug if slug == "" { slug = "topic" } return strings.TrimSuffix(baseURL, "/") + "/t/" + slug + "/" + fmt.Sprint(r.TopicID) } // CreateTopic posts a new topic into the category and returns its ids. func (c *Client) CreateTopic(ctx context.Context, req CreateTopicRequest) (*CreateTopicResult, error) { if strings.TrimSpace(req.Title) == "" { return nil, fmt.Errorf("%w: topic title is required", ErrInvalidRequest) } if req.Category == 0 { return nil, fmt.Errorf("%w: topic category id is required", ErrInvalidRequest) } var out CreateTopicResult if err := c.Post(ctx, "/posts.json", req, &out); err != nil { return nil, err } if out.TopicID == 0 { return nil, fmt.Errorf("%w: topic created but response carries no topic_id", ErrMalformedResponse) } return &out, nil } // ListTopics returns the topic list of a category, addressed by id // (GET /c/.json) or slug (GET /c/.json). Pass slug="" with id>0 // or id=0 with a slug. func (c *Client) ListTopics(ctx context.Context, id int, slug string) ([]TopicSummary, error) { if id == 0 && slug == "" { return nil, fmt.Errorf("%w: category id or slug is required", ErrInvalidRequest) } var path string if id > 0 { if slug != "" { path = "/c/" + quotePathSegment(slug) + "/" + fmt.Sprint(id) + ".json" } else { path = "/c/" + fmt.Sprint(id) + ".json" } } else { path = "/c/" + quotePathSegment(slug) + ".json" } var payload struct { TopicList struct { Topics []TopicSummary `json:"topics"` } `json:"topic_list"` } if err := c.Get(ctx, path, &payload); err != nil { return nil, err } return payload.TopicList.Topics, nil } // LatestTopics returns the instance's latest topic list (GET /latest.json). func (c *Client) LatestTopics(ctx context.Context) ([]TopicSummary, error) { var payload struct { TopicList struct { Topics []TopicSummary `json:"topics"` } `json:"topic_list"` } if err := c.Get(ctx, "/latest.json", &payload); err != nil { return nil, err } return payload.TopicList.Topics, nil } // GetTopic fetches one topic by id, posts included. func (c *Client) GetTopic(ctx context.Context, id int) (*Topic, error) { if id <= 0 { return nil, fmt.Errorf("%w: topic id must be > 0", ErrInvalidRequest) } var topic Topic if err := c.Get(ctx, "/t/"+fmt.Sprint(id)+".json", &topic); err != nil { return nil, err } return &topic, nil }