Skip to content
This repository was archived by the owner on Mar 12, 2026. It is now read-only.

Commit 7c31ece

Browse files
authored
Merge pull request #145 from nnemirovsky/feat/campfire-html-content-type
feat: enable rich text formatting for campfire posts
2 parents ec108bc + 679f882 commit 7c31ece

9 files changed

Lines changed: 20 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ bc4 supports Markdown input for creating content that gets automatically convert
795795
- ✅ **Messages** - List, post, view, and edit messages on project message boards
796796
- ✅ **Comments** - Create and edit comments with Markdown formatting
797797
- ✅ **Documents** - Create and edit documents with Markdown formatting
798-
- **Campfire** - Plain text only (API limitation)
798+
- **Campfire** - Post messages with Markdown formatting
799799
800800
### Supported Markdown Elements
801801
- **Bold** (`**text**`), *italic* (`*text*`), ~~strikethrough~~ (`~~text~~`)

cmd/campfire/post.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/needmore/bc4/internal/api"
1010
"github.com/needmore/bc4/internal/factory"
11+
"github.com/needmore/bc4/internal/markdown"
1112
"github.com/needmore/bc4/internal/parser"
1213
"github.com/spf13/cobra"
1314
)
@@ -105,17 +106,14 @@ func newPostCmd(f *factory.Factory) *cobra.Command {
105106
return fmt.Errorf("message cannot be empty")
106107
}
107108

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

117115
// Post the message
118-
line, err := campfireOps.PostCampfireLine(f.Context(), projectID, campfireID, content)
116+
line, err := campfireOps.PostCampfireLine(f.Context(), projectID, campfireID, richContent, "text/html")
119117
if err != nil {
120118
return fmt.Errorf("failed to post message: %w", err)
121119
}

docs/SPEC.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ According to the Basecamp API, the following resources support rich text content
404404
**Currently Implemented:**
405405
- ✅ **Todo** - `content` (title) and `description` fields support Markdown input
406406
- ✅ **Comment** - `content` field supports Markdown input and bc-attachment tags
407+
- ✅ **Campfire line** - uses `content_type: "text/html"` to send rich text
407408
408409
**Future Implementation:**
409410
- 🔄 **Card** - `title` and `content` fields will support Markdown
@@ -413,9 +414,6 @@ According to the Basecamp API, the following resources support rich text content
413414
- 🔄 **Upload** - `description` field will support Markdown
414415
- 🔄 **To-do list** - `description` field will support Markdown
415416
416-
**Not Supported (API Limitation):**
417-
- ❌ **Campfire line** - `content` field is plain text only per API specification
418-
419417
### Markdown to Rich Text Conversion
420418
421419
The `internal/markdown` package provides bidirectional conversion:

internal/api/campfire.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type CampfireLine struct {
4343

4444
// CampfireLineCreate represents the request body for creating a campfire line
4545
type CampfireLineCreate struct {
46-
Content string `json:"content"`
46+
Content string `json:"content"`
47+
ContentType string `json:"content_type,omitempty"`
4748
}
4849

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

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

103104
payload := CampfireLineCreate{
104-
Content: content,
105+
Content: content,
106+
ContentType: contentType,
105107
}
106108

107109
if err := c.Post(path, payload, &line); err != nil {

internal/api/interface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type APIClient interface {
3131
GetCampfire(ctx context.Context, projectID string, campfireID int64) (*Campfire, error)
3232
GetCampfireByName(ctx context.Context, projectID string, name string) (*Campfire, error)
3333
GetCampfireLines(ctx context.Context, projectID string, campfireID int64, limit int) ([]CampfireLine, error)
34-
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*CampfireLine, error)
34+
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*CampfireLine, error)
3535
DeleteCampfireLine(ctx context.Context, projectID string, campfireID int64, lineID int64) error
3636

3737
// Card table methods

internal/api/mock/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,8 @@ func (m *MockClient) GetCampfireLines(ctx context.Context, projectID string, cam
296296
}
297297

298298
// PostCampfireLine mock implementation
299-
func (m *MockClient) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*api.CampfireLine, error) {
300-
m.Calls = append(m.Calls, fmt.Sprintf("PostCampfireLine(%s, %d, %s)", projectID, campfireID, content))
299+
func (m *MockClient) PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*api.CampfireLine, error) {
300+
m.Calls = append(m.Calls, fmt.Sprintf("PostCampfireLine(%s, %d, %s, %s)", projectID, campfireID, content, contentType))
301301
if m.PostCampfireLineError != nil {
302302
return nil, m.PostCampfireLineError
303303
}

internal/api/modular.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type CampfireOperations interface {
4242
GetCampfire(ctx context.Context, projectID string, campfireID int64) (*Campfire, error)
4343
GetCampfireByName(ctx context.Context, projectID string, name string) (*Campfire, error)
4444
GetCampfireLines(ctx context.Context, projectID string, campfireID int64, limit int) ([]CampfireLine, error)
45-
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string) (*CampfireLine, error)
45+
PostCampfireLine(ctx context.Context, projectID string, campfireID int64, content string, contentType string) (*CampfireLine, error)
4646
DeleteCampfireLine(ctx context.Context, projectID string, campfireID int64, lineID int64) error
4747
}
4848

internal/markdown/converter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func NewConverter() Converter {
3939
),
4040
goldmark.WithRendererOptions(
4141
html.WithXHTML(),
42+
html.WithHardWraps(),
4243
),
4344
)
4445

internal/markdown/converter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func TestMarkdownToRichText(t *testing.T) {
125125
{
126126
name: "multiline blockquote",
127127
input: "> Line 1\n> Line 2",
128-
expected: "<blockquote><div>Line 1\nLine 2</div></blockquote>",
128+
expected: "<blockquote><div>Line 1<br>Line 2</div></blockquote>",
129129
},
130130
{
131131
name: "blockquote with formatting",

0 commit comments

Comments
 (0)