Skip to content
This repository was archived by the owner on Mar 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ bc4 supports Markdown input for creating content that gets automatically convert
- ✅ **Messages** - List, post, view, and edit messages on project message boards
- ✅ **Comments** - Create and edit comments with Markdown formatting
- ✅ **Documents** - Create and edit documents with Markdown formatting
- **Campfire** - Plain text only (API limitation)
- **Campfire** - Post messages with Markdown formatting

### Supported Markdown Elements
- **Bold** (`**text**`), *italic* (`*text*`), ~~strikethrough~~ (`~~text~~`)
Expand Down
16 changes: 7 additions & 9 deletions cmd/campfire/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/parser"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -105,17 +106,14 @@ func newPostCmd(f *factory.Factory) *cobra.Command {
return fmt.Errorf("message cannot be empty")
}

// For now, just post the plain content
// The API seems to be escaping HTML in campfire messages
// TODO: Investigate the correct way to send rich text to campfire
// converter := markdown.NewConverter()
// richContent, err := converter.MarkdownToRichText(content)
// if err != nil {
// return fmt.Errorf("failed to convert message: %w", err)
// }
converter := markdown.NewConverter()
richContent, err := converter.MarkdownToRichText(content)
if err != nil {
return fmt.Errorf("failed to convert message: %w", err)
}

// Post the message
line, err := campfireOps.PostCampfireLine(f.Context(), projectID, campfireID, content)
line, err := campfireOps.PostCampfireLine(f.Context(), projectID, campfireID, richContent, "text/html")
if err != nil {
return fmt.Errorf("failed to post message: %w", err)
}
Expand Down
4 changes: 1 addition & 3 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ According to the Basecamp API, the following resources support rich text content
**Currently Implemented:**
- ✅ **Todo** - `content` (title) and `description` fields support Markdown input
- ✅ **Comment** - `content` field supports Markdown input and bc-attachment tags
- ✅ **Campfire line** - uses `content_type: "text/html"` to send rich text

**Future Implementation:**
- 🔄 **Card** - `title` and `content` fields will support Markdown
Expand All @@ -413,9 +414,6 @@ According to the Basecamp API, the following resources support rich text content
- 🔄 **Upload** - `description` field will support Markdown
- 🔄 **To-do list** - `description` field will support Markdown

**Not Supported (API Limitation):**
- ❌ **Campfire line** - `content` field is plain text only per API specification

### Markdown to Rich Text Conversion

The `internal/markdown` package provides bidirectional conversion:
Expand Down
8 changes: 5 additions & 3 deletions internal/api/campfire.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type CampfireLine struct {

// CampfireLineCreate represents the request body for creating a campfire line
type CampfireLineCreate struct {
Content string `json:"content"`
Content string `json:"content"`
ContentType string `json:"content_type,omitempty"`
}

// ListCampfires returns all campfires for a project
Expand Down Expand Up @@ -96,12 +97,13 @@ func (c *Client) GetCampfireLines(ctx context.Context, projectID string, campfir
}

// PostCampfireLine posts a new message to a campfire
func (c *Client) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*CampfireLine, error) {
func (c *Client) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*CampfireLine, error) {
var line CampfireLine
path := fmt.Sprintf("/buckets/%s/chats/%d/lines.json", projectID, campfireID)

payload := CampfireLineCreate{
Content: content,
Content: content,
ContentType: contentType,
}

if err := c.Post(path, payload, &line); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type APIClient interface {
GetCampfire(ctx context.Context, projectID string, campfireID int64) (*Campfire, error)
GetCampfireByName(ctx context.Context, projectID string, name string) (*Campfire, error)
GetCampfireLines(ctx context.Context, projectID string, campfireID int64, limit int) ([]CampfireLine, error)
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*CampfireLine, error)
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*CampfireLine, error)
DeleteCampfireLine(ctx context.Context, projectID string, campfireID int64, lineID int64) error

// Card table methods
Expand Down
4 changes: 2 additions & 2 deletions internal/api/mock/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ func (m *MockClient) GetCampfireLines(ctx context.Context, projectID string, cam
}

// PostCampfireLine mock implementation
func (m *MockClient) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*api.CampfireLine, error) {
m.Calls = append(m.Calls, fmt.Sprintf("PostCampfireLine(%s, %d, %s)", projectID, campfireID, content))
func (m *MockClient) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*api.CampfireLine, error) {
m.Calls = append(m.Calls, fmt.Sprintf("PostCampfireLine(%s, %d, %s, %s)", projectID, campfireID, content, contentType))
if m.PostCampfireLineError != nil {
return nil, m.PostCampfireLineError
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/modular.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type CampfireOperations interface {
GetCampfire(ctx context.Context, projectID string, campfireID int64) (*Campfire, error)
GetCampfireByName(ctx context.Context, projectID string, name string) (*Campfire, error)
GetCampfireLines(ctx context.Context, projectID string, campfireID int64, limit int) ([]CampfireLine, error)
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*CampfireLine, error)
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*CampfireLine, error)
DeleteCampfireLine(ctx context.Context, projectID string, campfireID int64, lineID int64) error
}

Expand Down
1 change: 1 addition & 0 deletions internal/markdown/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func NewConverter() Converter {
),
goldmark.WithRendererOptions(
html.WithXHTML(),
html.WithHardWraps(),
),
)

Expand Down
2 changes: 1 addition & 1 deletion internal/markdown/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func TestMarkdownToRichText(t *testing.T) {
{
name: "multiline blockquote",
input: "> Line 1\n> Line 2",
expected: "<blockquote><div>Line 1\nLine 2</div></blockquote>",
expected: "<blockquote><div>Line 1<br>Line 2</div></blockquote>",
},
{
name: "blockquote with formatting",
Expand Down
Loading