// Posts: create (POST /posts.json — replies or topic creation, the same // endpoint), update (PUT /posts/.json), and the session identity // check (GET /session/current.json) used by smoke tests and health probes. package discourse import ( "context" "fmt" "strings" ) // Post is the post payload subset callers use. type Post struct { ID int `json:"id"` TopicID int `json:"topic_id"` PostNumber int `json:"post_number"` Raw string `json:"raw"` Cooked string `json:"cooked"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // CreatePostRequest posts into an existing topic (topic_id set) or // creates a topic (topic_id zero + title + category — prefer // CreateTopic for that path). type CreatePostRequest struct { TopicID int `json:"topic_id"` Raw string `json:"raw"` Title string `json:"title,omitempty"` Category int `json:"category,omitempty"` } // CreatePostResult is the POST /posts.json response. type CreatePostResult struct { PostID int `json:"id"` TopicID int `json:"topic_id"` TopicSlug string `json:"topic_slug"` } // CreatePost posts markdown into a topic and returns the ids. func (c *Client) CreatePost(ctx context.Context, req CreatePostRequest) (*CreatePostResult, error) { if req.TopicID <= 0 { return nil, fmt.Errorf("%w: topic_id is required (use CreateTopic for new topics)", ErrInvalidRequest) } if strings.TrimSpace(req.Raw) == "" { return nil, fmt.Errorf("%w: post raw body is required", ErrInvalidRequest) } var out CreatePostResult if err := c.Post(ctx, "/posts.json", req, &out); err != nil { return nil, err } return &out, nil } // UpdatePost replaces a post's markdown body (PUT /posts/.json) with // an optional edit reason. func (c *Client) UpdatePost(ctx context.Context, postID int, raw, editReason string) (*Post, error) { if postID <= 0 { return nil, fmt.Errorf("%w: post id must be > 0", ErrInvalidRequest) } if strings.TrimSpace(raw) == "" { return nil, fmt.Errorf("%w: post raw body is required", ErrInvalidRequest) } body := map[string]any{ "post": map[string]any{ "raw": raw, }, } if editReason != "" { body["post"].(map[string]any)["edit_reason"] = editReason } var post struct { Post Post `json:"post"` } if err := c.Put(ctx, "/posts/"+fmt.Sprint(postID)+".json", body, &post); err != nil { return nil, err } if post.Post.ID == 0 { return nil, fmt.Errorf("%w: post updated but response carries no id", ErrMalformedResponse) } return &post.Post, nil } // GetPost fetches one post by id (GET /posts/.json). func (c *Client) GetPost(ctx context.Context, postID int) (*Post, error) { if postID <= 0 { return nil, fmt.Errorf("%w: post id must be > 0", ErrInvalidRequest) } var payload struct { Post Post `json:"post"` } if err := c.Get(ctx, "/posts/"+fmt.Sprint(postID)+".json", &payload); err != nil { return nil, err } if payload.Post.ID == 0 { return nil, fmt.Errorf("%w: post response carries no id", ErrMalformedResponse) } return &payload.Post, nil } // CurrentUser is the identity the key acts as — the cheapest live // verification of url + key + username (any 2xx means the trio works). type CurrentUser struct { ID int `json:"id"` Username string `json:"username"` Admin bool `json:"admin"` } // CurrentUser runs GET /session/current.json. func (c *Client) CurrentUser(ctx context.Context) (*CurrentUser, error) { var payload struct { CurrentUser CurrentUser `json:"current_user"` } if err := c.Get(ctx, "/session/current.json", &payload); err != nil { return nil, err } if payload.CurrentUser.Username == "" { return nil, fmt.Errorf("%w: no current_user in response", ErrMalformedResponse) } return &payload.CurrentUser, nil }