Skip to content
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 cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi

// Slack endpoints - routed to workspace-specific clients
router.HandleFunc("/slack/events", eventRouter.HandleEvents).Methods("POST")
router.HandleFunc("/slack/interactions", eventRouter.HandleInteractions).Methods("POST")
router.HandleFunc("/slack/interactive-endpoint", eventRouter.HandleInteractions).Methods("POST")
router.HandleFunc("/slack/slash", eventRouter.HandleSlashCommand).Methods("POST")

// Determine port.
Expand Down
2 changes: 1 addition & 1 deletion internal/bot/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type SlackClient interface {
IsBotInChannel(ctx context.Context, channelID string) bool
BotInfo(ctx context.Context) (*slack.AuthTestResponse, error)
WorkspaceInfo(ctx context.Context) (*slack.TeamInfo, error)
PublishHomeView(userID string, blocks []slack.Block) error
PublishHomeView(ctx context.Context, userID string, blocks []slack.Block) error
API() *slack.Client
}

Expand Down
42 changes: 36 additions & 6 deletions internal/slack/events_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"log/slog"
"net/http"
"net/url"

"github.com/slack-go/slack/slackevents"
)
Expand Down Expand Up @@ -87,7 +88,11 @@ func (er *EventRouter) HandleEvents(w http.ResponseWriter, req *http.Request) {
"team_id", teamID,
"event_type", eventWrapper.Type,
"remote_addr", req.RemoteAddr,
"user_agent", req.Header.Get("User-Agent"))
"user_agent", req.Header.Get("User-Agent"),
"signature_present", signature != "",
"timestamp", timestamp,
"body_size", len(body),
"response_status", http.StatusUnauthorized)
w.WriteHeader(http.StatusUnauthorized)
return
}
Expand All @@ -112,12 +117,31 @@ func (er *EventRouter) HandleInteractions(w http.ResponseWriter, req *http.Reque
return
}

// Log raw body for debugging (truncate if too large)
bodyPreview := string(body)
if len(bodyPreview) > 500 {
bodyPreview = bodyPreview[:500] + "... (truncated)"
}
slog.Debug("received interaction request",
"body_size", len(body),
"raw_body", bodyPreview,
"remote_addr", req.RemoteAddr)

// Parse payload to extract team_id FIRST (before signature verification)
// Interactions come as form-encoded with a "payload" field
payload := req.FormValue("payload")
// We must parse from body bytes since body was already read
values, err := url.ParseQuery(string(body))
if err != nil {
slog.Error("failed to parse form data", "error", err)
w.WriteHeader(http.StatusBadRequest)
return
}

payload := values.Get("payload")
if payload == "" {
// Try reading from body
payload = string(body)
slog.Error("interaction missing payload field")
w.WriteHeader(http.StatusBadRequest)
return
}

var interaction struct {
Expand Down Expand Up @@ -155,12 +179,18 @@ func (er *EventRouter) HandleInteractions(w http.ResponseWriter, req *http.Reque
slog.Warn("interaction signature verification failed - possible attack",
"team_id", teamID,
"remote_addr", req.RemoteAddr,
"user_agent", req.Header.Get("User-Agent"))
"user_agent", req.Header.Get("User-Agent"),
"signature_present", signature != "",
"timestamp", timestamp,
"body_size", len(body),
"response_status", http.StatusUnauthorized)
w.WriteHeader(http.StatusUnauthorized)
return
}

slog.Debug("routing interaction to workspace", "team_id", teamID)
slog.Debug("routing interaction to workspace",
"team_id", teamID,
"body_size", len(body))

// Forward to the workspace-specific client's interaction handler
req.Body = io.NopCloser(&readerWrapper{data: body})
Expand Down
53 changes: 44 additions & 9 deletions internal/slack/home_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package slack

import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
Expand Down Expand Up @@ -37,10 +38,38 @@ func NewHomeHandler(

// HandleAppHomeOpened updates the app home view when a user opens it.
func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUserID string) error {
slog.Debug("handling app home opened",
slog.Info("handling app home opened - fetching fresh data",
"team_id", teamID,
"slack_user_id", slackUserID)

// Try up to 2 times - first with cached client, second with fresh client after invalid_auth
for attempt := 0; attempt < 2; attempt++ {
if attempt > 0 {
slog.Info("retrying home view after invalid_auth", "team_id", teamID, "attempt", attempt+1)
}

err := h.tryHandleAppHomeOpened(ctx, teamID, slackUserID)
if err == nil {
return nil
}

// If invalid_auth and first attempt, invalidate cache and retry
if strings.Contains(err.Error(), "invalid_auth") && attempt == 0 {
slog.Warn("invalid_auth detected - invalidating cache and retrying",
"team_id", teamID)
h.slackManager.InvalidateCache(teamID)
continue
}

// Other errors or second attempt - return immediately
return err
}

return errors.New("failed after retries")
}

// tryHandleAppHomeOpened attempts to handle app home opened event.
func (h *HomeHandler) tryHandleAppHomeOpened(ctx context.Context, teamID, slackUserID string) error {
// Get Slack client for this workspace
slackClient, err := h.slackManager.Client(ctx, teamID)
if err != nil {
Expand All @@ -50,8 +79,12 @@ func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUser
// Get Slack user info to extract email
slackUser, err := slackClient.API().GetUserInfo(slackUserID)
if err != nil {
// Don't mask invalid_auth errors - let them propagate for retry logic
if strings.Contains(err.Error(), "invalid_auth") {
return fmt.Errorf("failed to get Slack user info: %w", err)
}
slog.Warn("failed to get Slack user info", "user_id", slackUserID, "error", err)
return h.publishPlaceholderHome(slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
}

// Extract GitHub username from email (simple heuristic: part before @)
Expand All @@ -62,15 +95,15 @@ func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUser
slog.Warn("could not extract GitHub username from Slack email",
"slack_user_id", slackUserID,
"email", email)
return h.publishPlaceholderHome(slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
}
githubUsername := email[:atIndex]

// Get all orgs for this workspace
workspaceOrgs := h.workspaceOrgs(teamID)
if len(workspaceOrgs) == 0 {
slog.Warn("no workspace orgs found", "team_id", teamID)
return h.publishPlaceholderHome(slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
}

// Get GitHub client for first org (they all share the same app)
Expand All @@ -92,7 +125,7 @@ func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUser
slog.Error("failed to fetch dashboard",
"github_user", githubUsername,
"error", err)
return h.publishPlaceholderHome(slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
}

// Add workspace orgs to dashboard for UI display
Expand All @@ -102,15 +135,17 @@ func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUser
blocks := home.BuildBlocks(dashboard, workspaceOrgs[0])

// Publish to Slack
if err := slackClient.PublishHomeView(slackUserID, blocks); err != nil {
if err := slackClient.PublishHomeView(ctx, slackUserID, blocks); err != nil {
return fmt.Errorf("failed to publish home view: %w", err)
}

slog.Info("published home view",
slog.Info("published home view with fresh data",
"slack_user_id", slackUserID,
"github_user", githubUsername,
"incoming_prs", len(dashboard.IncomingPRs),
"incoming_blocked", dashboard.Counts().IncomingBlocked,
"outgoing_prs", len(dashboard.OutgoingPRs),
"outgoing_blocked", dashboard.Counts().OutgoingBlocked,
"workspace_orgs", len(workspaceOrgs))

return nil
Expand All @@ -137,7 +172,7 @@ func (h *HomeHandler) workspaceOrgs(teamID string) []string {
}

// publishPlaceholderHome publishes a simple placeholder home view.
func (*HomeHandler) publishPlaceholderHome(slackClient *Client, slackUserID string) error {
func (*HomeHandler) publishPlaceholderHome(ctx context.Context, slackClient *Client, slackUserID string) error {
slog.Debug("publishing placeholder home", "user_id", slackUserID)

blocks := home.BuildBlocks(&home.Dashboard{
Expand All @@ -146,5 +181,5 @@ func (*HomeHandler) publishPlaceholderHome(slackClient *Client, slackUserID stri
WorkspaceOrgs: []string{"your-org"},
}, "your-org")

return slackClient.PublishHomeView(slackUserID, blocks)
return slackClient.PublishHomeView(ctx, slackUserID, blocks)
}
1 change: 1 addition & 0 deletions internal/slack/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (m *Manager) Client(ctx context.Context, teamID string) (*Client, error) {
// Create client
client = New(token, m.signingSecret)
client.SetTeamID(teamID)
client.SetManager(m) // Set manager reference for cache invalidation

// Set home view handler if configured
if m.homeViewHandler != nil {
Expand Down
Loading