From 164c73d2bea0f1beaaa43c3ded63a4d5f0fdee6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=A3=9E=E8=99=B9?= Date: Wed, 25 Feb 2026 02:26:56 +0800 Subject: [PATCH 1/3] docs: add v0.11.0 release notes to CHANGELOG - Add comprehensive v0.11.0 changelog with all P0+P1 fixes - Document path traversal protection and ACK retry mechanism - Include test coverage statistics (93.8%+) - Link to PR #23, Issue #21, and release notes - Follow Keep a Changelog format --- CHANGELOG.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f795a7a..d356b158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,95 @@ +## [v0.11.0] - 2026-02-25 + +### ๐Ÿ” Slack ๅฎ‰ๅ…จๅขžๅผบไธŽๅฏ้ ๆ€งๆๅ‡ + +This release addresses critical security and reliability gaps identified in the [Slack Gap Analysis Report](docs/chatapps/slack-gap-analysis.md). We've implemented comprehensive path traversal protection, Socket Mode ACK retry mechanism, and extensive documentation. + +### Added +- **Path Traversal Attack Protection**: + - New `expandPath()` function with `~` expansion to user home directory + - New `isSensitivePath()` function blocking access to system directories (`/etc`, `/var`, `/usr`, `/bin`, `/sbin`, `/root`, `/proc`, `/sys`, `/boot`, `/dev`) + - Automatic detection and blocking of path traversal attempts (e.g., `../etc/passwd`) + - Safe path cleaning with `filepath.Clean` for relative paths + +- **Socket Mode ACK Retry Mechanism**: + - New `sendACKWithRetry()` function with exponential backoff (1s โ†’ 2s โ†’ 4s) + - Maximum 3 retries (4 total attempts) for reliable message delivery + - Slack API compliant 3-second response requirement + - Comprehensive logging for debugging connection issues + +- **Comprehensive Unit Tests**: + - 26 test cases for `expandPath()` covering normal paths, edge cases, and security scenarios + - 10 test cases for `isSensitivePath()` covering all blocked directories + - 93.8% test coverage for path handling functions + - New test file `chatapps/setup_test.go` (+279 lines) + +- **Gap Analysis Report** ([Issue #21](https://github.com/hrygo/hotplex/issues/21)): + - Comprehensive 416-line comparison: HotPlex vs OpenClaw Slack implementations + - 30+ feature gaps identified across 6 categories (P0/P1/P2 priority) + - 3-phase implementation roadmap (14-20 weeks estimated) + - Technical debt risk identification + +- **Documentation Updates**: + - System prompt configuration guide with injection flow diagram + - Security features documentation (path checks, ACK retry, signature verification) + - Troubleshooting examples (Q5: System prompt not็”Ÿๆ•ˆ๏ผŒQ6: Path blocked) + - Example environment files (`.env.development`, `.env.production`) + +### Changed +- **Configuration Enhancements** (`chatapps/configs/slack.yaml`): + - Detailed path security documentation with examples + - ACK retry mechanism explanation + - System prompt injection flow description + - Complete troubleshooting section + +- **User Manual** (`docs/chatapps/chatapps-slack.md`): + - Added Chapter 7: System Prompt Configuration + - Added Chapter 8: Security Features + - Updated changelog with v0.10.0, v0.9.0, v0.8.0 + +- **Code Quality**: + - Project-wide lint cleanup + - Improved error handling in path expansion + - Enhanced logging for security events + +### Fixed +- **Duplicate Message Processing** ([PR #23](https://github.com/hrygo/hotplex/pull/23)): + - Removed duplicate `handleEventsAPI()` call in Socket Mode + - Added empty payload validation + - Fixed potential message duplication issue + +- **Security Vulnerabilities**: + - Blocked access to sensitive system directories + - Prevented path traversal attacks via `..` sequences + - Hardened path validation with multiple security layers + +### Technical Details +- **Files Changed**: 7 files +- **Lines Added**: +1,256 +- **Lines Removed**: -140 +- **Net Change**: +1,116 lines +- **Test Coverage**: 93.8%+ (41 test cases) + +### Verification +```bash +โœ… go test ./... - All tests pass +โœ… go build ./... - Build succeeds +โœ… golangci-lint run - 0 issues +โœ… Path security - Blocks /etc, /var, /root successfully +โœ… ACK retry - Handles connection failures +``` + +### Contributors +- [@hrygo](https://github.com/hrygo) + +### Related +- **PR**: [#23](https://github.com/hrygo/hotplex/pull/23) +- **Issue**: [#21](https://github.com/hrygo/hotplex/issues/21) +- **Release**: [v0.11.0](https://github.com/hrygo/hotplex/releases/tag/v0.11.0) +- **Gap Analysis**: [docs/chatapps/slack-gap-analysis.md](docs/chatapps/slack-gap-analysis.md) + +--- + ## [v0.10.0] - 2026-02-23 ### ๐Ÿš€ ChatApps-as-a-Service Milestone (v0.10.0) From 5369670a93e03231394738983b2683ecdfa0c709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=A3=9E=E8=99=B9?= Date: Wed, 25 Feb 2026 02:33:21 +0800 Subject: [PATCH 2/3] feat(slack): add reaction support - Add Reaction type to base.RichContent - Add AddReaction method to Slack adapter - Integrate reactions into defaultSender --- chatapps/base/types.go | 8 ++++ chatapps/slack/adapter.go | 65 +++++++++++++++++++++++++++++++++ docs-site/.vitepress/config.mts | 4 +- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/chatapps/base/types.go b/chatapps/base/types.go index 37d3ae3d..b6552b89 100644 --- a/chatapps/base/types.go +++ b/chatapps/base/types.go @@ -23,6 +23,14 @@ type RichContent struct { Blocks []any Embeds []any Attachments []Attachment + Reactions []Reaction +} + +// Reaction represents a reaction to add to a message +type Reaction struct { + Name string // emoji name (e.g., "thumbsup", "+1") + Channel string + Timestamp string // message timestamp to react to } type Attachment struct { diff --git a/chatapps/slack/adapter.go b/chatapps/slack/adapter.go index 9424ecf6..4bafabe0 100644 --- a/chatapps/slack/adapter.go +++ b/chatapps/slack/adapter.go @@ -108,6 +108,16 @@ func (a *Adapter) defaultSender(ctx context.Context, sessionID string, msg *base } } + // Send reactions if present + if msg.RichContent != nil && len(msg.RichContent.Reactions) > 0 { + for _, reaction := range msg.RichContent.Reactions { + reaction.Channel = channelID + if err := a.AddReaction(ctx, reaction); err != nil { + a.Logger().Error("Failed to add reaction", "error", err, "reaction", reaction.Name) + } + } + } + // Send media/attachments if present if msg.RichContent != nil && len(msg.RichContent.Attachments) > 0 { for _, attachment := range msg.RichContent.Attachments { @@ -530,3 +540,58 @@ func (a *Adapter) sendToChannelOnce(ctx context.Context, channelID, text, thread a.Logger().Debug("Message sent successfully", "channel", channelID) return nil } + +// AddReaction adds a reaction to a message +func (a *Adapter) AddReaction(ctx context.Context, reaction base.Reaction) error { + if a.config.BotToken == "" { + return fmt.Errorf("slack bot token not configured") + } + + if reaction.Channel == "" || reaction.Timestamp == "" { + return fmt.Errorf("channel and timestamp are required for reaction") + } + + payload := map[string]any{ + "channel": reaction.Channel, + "name": reaction.Name, + "ts": reaction.Timestamp, + } + + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal payload: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", "https://slack.com/api/reactions.add", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.config.BotToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("send request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode >= 400 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("reaction add failed: %d %s", resp.StatusCode, string(respBody)) + } + + var slackResp struct { + OK bool `json:"ok"` + Error string `json:"error,omitempty"` + } + if err := json.NewDecoder(resp.Body).Decode(&slackResp); err != nil { + return fmt.Errorf("parse response: %w", err) + } + + if !slackResp.OK { + return fmt.Errorf("slack API error: %s", slackResp.Error) + } + + a.Logger().Debug("Reaction added", "emoji", reaction.Name, "channel", reaction.Channel) + return nil +} diff --git a/docs-site/.vitepress/config.mts b/docs-site/.vitepress/config.mts index b3c0f0a8..f860cb7c 100644 --- a/docs-site/.vitepress/config.mts +++ b/docs-site/.vitepress/config.mts @@ -56,7 +56,9 @@ export default defineConfig({ { text: 'WebSocket Protocol', link: '/guide/websocket' }, { text: 'OpenCode HTTP/SSE', link: '/guide/opencode-http' }, { text: 'ChatApps Overview', link: '/guide/chatapps' }, - { text: 'โ””โ”€ DingTalk Deep Dive', link: '/guide/chatapps-dingtalk' } + { text: 'โ””โ”€ DingTalk Deep Dive', link: '/guide/chatapps-dingtalk' }, + { text: 'โ””โ”€ Slack Deep Dive', link: '/guide/chatapps-slack' }, + { text: 'โ””โ”€ Slack Gap Analysis', link: '/guide/slack-gap-analysis' } ] }, { From 5b0f33a894e003e85e68d0a3398d833c475683e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=A3=9E=E8=99=B9?= Date: Wed, 25 Feb 2026 02:36:58 +0800 Subject: [PATCH 3/3] feat(slack): add slash command support - Add slash command handler at /slack endpoint - Add SlashCommand type and SetSlashCommandHandler --- chatapps/slack/adapter.go | 72 +++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/chatapps/slack/adapter.go b/chatapps/slack/adapter.go index 4bafabe0..e1d2c81c 100644 --- a/chatapps/slack/adapter.go +++ b/chatapps/slack/adapter.go @@ -19,12 +19,14 @@ import ( type Adapter struct { *base.Adapter - config Config - eventPath string - interactivePath string - sender *base.SenderWithMutex - webhook *base.WebhookRunner - socketMode *SocketModeConnection + config Config + eventPath string + interactivePath string + slashCommandPath string + sender *base.SenderWithMutex + webhook *base.WebhookRunner + socketMode *SocketModeConnection + slashCommandHandler func(cmd SlashCommand) } func NewAdapter(config Config, logger *slog.Logger, opts ...base.AdapterOption) *Adapter { @@ -34,11 +36,12 @@ func NewAdapter(config Config, logger *slog.Logger, opts ...base.AdapterOption) } a := &Adapter{ - config: config, - eventPath: "/events", - interactivePath: "/interactive", - sender: base.NewSenderWithMutex(), - webhook: base.NewWebhookRunner(logger), + config: config, + eventPath: "/events", + interactivePath: "/interactive", + slashCommandPath: "/slack", + sender: base.NewSenderWithMutex(), + webhook: base.NewWebhookRunner(logger), } // Initialize Socket Mode if configured @@ -61,6 +64,7 @@ func NewAdapter(config Config, logger *slog.Logger, opts ...base.AdapterOption) // Slack recommends using both Socket Mode and HTTP webhook together handlers[a.eventPath] = a.handleEvent handlers[a.interactivePath] = a.handleInteractive + handlers[a.slashCommandPath] = a.handleSlashCommand // Build HTTP handler map for path, handler := range handlers { @@ -595,3 +599,49 @@ func (a *Adapter) AddReaction(ctx context.Context, reaction base.Reaction) error a.Logger().Debug("Reaction added", "emoji", reaction.Name, "channel", reaction.Channel) return nil } + +// SlashCommand represents a Slack slash command +type SlashCommand struct { + Command string + Text string + UserID string + ChannelID string + ResponseURL string +} + +// SetSlashCommandHandler sets the handler for slash commands +func (a *Adapter) SetSlashCommandHandler(fn func(cmd SlashCommand)) { + a.slashCommandHandler = fn +} + +// handleSlashCommand processes incoming slash commands +func (a *Adapter) handleSlashCommand(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + if err := r.ParseForm(); err != nil { + a.Logger().Error("Parse slash command form failed", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + cmd := SlashCommand{ + Command: r.FormValue("command"), + Text: r.FormValue("text"), + UserID: r.FormValue("user_id"), + ChannelID: r.FormValue("channel_id"), + ResponseURL: r.FormValue("response_url"), + } + + a.Logger().Debug("Slash command received", "command", cmd.Command, "text", cmd.Text, "user", cmd.UserID) + + // Acknowledge immediately + w.WriteHeader(http.StatusOK) + + // Process in background if handler is set + if a.slashCommandHandler != nil { + go a.slashCommandHandler(cmd) + } +}