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
19 changes: 18 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/codeGROOVE-dev/slacker/pkg/notify"
"github.com/codeGROOVE-dev/slacker/pkg/slack"
"github.com/codeGROOVE-dev/slacker/pkg/state"
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
"github.com/codeGROOVE-dev/sprinkler/pkg/client"
"github.com/gorilla/mux"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -244,8 +245,24 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi
// Initialize event router for multi-workspace event handling.
eventRouter := slack.NewEventRouter(slackManager)

// Initialize reverse user mapping service (Slack → GitHub)
// Get GitHub token from one of the installations
var githubToken string
for _, org := range githubManager.AllOrgs() {
if client, ok := githubManager.ClientForOrg(org); ok {
githubToken = client.InstallationToken(ctx)
break
}
}
if githubToken == "" {
slog.Warn("no GitHub installations found - reverse user mapping will not work")
}
// Pass nil for Slack client - it will be provided per-request in HomeHandler
reverseMapping := usermapping.NewReverseService(nil, githubToken)
slog.Info("initialized reverse user mapping service (Slack → GitHub)")

// Initialize home view handler
homeHandler := slack.NewHomeHandler(slackManager, githubManager, configManager, stateStore)
homeHandler := slack.NewHomeHandler(slackManager, githubManager, configManager, stateStore, reverseMapping)
slackManager.SetHomeViewHandler(homeHandler.HandleAppHomeOpened)

// Initialize OAuth handler for Slack app installation.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.1

require (
github.com/codeGROOVE-dev/ds9 v0.6.0
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4
github.com/codeGROOVE-dev/retry v1.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/codeGROOVE-dev/ds9 v0.6.0 h1:JG7vBH17UAKaVoeQilrIvA1I0fg3iNbdUMBSDS7ixgI=
github.com/codeGROOVE-dev/ds9 v0.6.0/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9 h1:eyWcEZd3xyLV2WxShoyKWakFyxQGvOSv89ponU3Ah0I=
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9/go.mod h1:4Hr2ySB8dcpeZqZq/7UbXdEJ/5RK9coYGHvW90ZfieE=
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928 h1:fDiQ7GnN6tDUIrYqXKCmxcatFzZqr+Bp3aNBA0Q2AVk=
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928/go.mod h1:4Hr2ySB8dcpeZqZq/7UbXdEJ/5RK9coYGHvW90ZfieE=
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22 h1:gtN3rOc6YspO646BkcOxBhPjEqKUz+jl175jIqglfDg=
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22/go.mod h1:KV+w19ubP32PxZPE1hOtlCpTaNpF0Bpb32w5djO8UTg=
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4 h1:DSuoUwP3oyR4cHrX0cUh9c7CtYjXNIcyCmqpIwHilIU=
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type RepoConfig struct {
ReminderDMDelay int `yaml:"reminder_dm_delay"` // Minutes to wait before sending DM if user tagged in channel (0 = disabled)
DailyReminders bool `yaml:"daily_reminders"`
} `yaml:"global"`
Users map[string]string `yaml:"users"` // GitHub username -> email address (for manual overrides)
}

// configCacheEntry represents a cached configuration entry.
Expand Down
59 changes: 59 additions & 0 deletions pkg/home/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
"time"

"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
"github.com/slack-go/slack"
)

Expand Down Expand Up @@ -226,3 +227,61 @@ func formatEnhancedPRBlock(pr *PR) slack.Block {
nil,
)
}

// BuildBlocksWithDebug creates Slack Block Kit UI with debug information about user mapping.
func BuildBlocksWithDebug(dashboard *Dashboard, primaryOrg string, mapping *usermapping.ReverseMapping) []slack.Block {
// Build standard blocks first
blocks := BuildBlocks(dashboard, primaryOrg)

// Add debug section if mapping info is available
if mapping != nil {
blocks = append(blocks,
slack.NewDividerBlock(),
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn",
fmt.Sprintf("🔍 *Debug Info*\n"+
"GitHub: `@%s` • Mapped via: `%s` • Confidence: `%d%%`",
mapping.GitHubUsername,
mapping.MatchMethod,
mapping.Confidence),
false,
false,
),
nil,
nil,
),
)

// Add mapping guidance if confidence is low
if mapping.Confidence < 80 {
blocks = append(blocks,
slack.NewContextBlock("",
slack.NewTextBlockObject("mrkdwn",
fmt.Sprintf("⚠️ Low confidence mapping. Add manual override to `slack.yaml`:\n```yaml\nusers:\n %s: %s\n```",
mapping.GitHubUsername,
mapping.SlackEmail),
false,
false,
),
),
)
}
} else {
// No mapping found - show error message
blocks = append(blocks,
slack.NewDividerBlock(),
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn",
"❌ *Could not map Slack user to GitHub*\n"+
"Add your mapping to `.codeGROOVE/slack.yaml`:\n```yaml\nusers:\n your-github-username: [email protected]\n```",
false,
false,
),
nil,
nil,
),
)
}

return blocks
}
79 changes: 41 additions & 38 deletions pkg/slack/home_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ import (
"github.com/codeGROOVE-dev/slacker/pkg/github"
"github.com/codeGROOVE-dev/slacker/pkg/home"
"github.com/codeGROOVE-dev/slacker/pkg/state"
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
gogithub "github.com/google/go-github/v50/github"
)

// HomeHandler handles app_home_opened events for a workspace.
type HomeHandler struct {
slackManager *Manager
githubManager *github.Manager
configManager *config.Manager
stateStore state.Store
slackManager *Manager
githubManager *github.Manager
configManager *config.Manager
stateStore state.Store
reverseMapping *usermapping.ReverseService
}

// NewHomeHandler creates a new home view handler.
Expand All @@ -28,12 +30,14 @@ func NewHomeHandler(
githubManager *github.Manager,
configManager *config.Manager,
stateStore state.Store,
reverseMapping *usermapping.ReverseService,
) *HomeHandler {
return &HomeHandler{
slackManager: slackManager,
githubManager: githubManager,
configManager: configManager,
stateStore: stateStore,
slackManager: slackManager,
githubManager: githubManager,
configManager: configManager,
stateStore: stateStore,
reverseMapping: reverseMapping,
}
}

Expand Down Expand Up @@ -77,36 +81,35 @@ func (h *HomeHandler) tryHandleAppHomeOpened(ctx context.Context, teamID, slackU
return fmt.Errorf("failed to get Slack client: %w", err)
}

// 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(ctx, slackClient, slackUserID)
}

// Extract GitHub username from email (simple heuristic: part before @)
// Works for "[email protected]" -> "username"
email := slackUser.Profile.Email
atIndex := strings.IndexByte(email, '@')
if atIndex <= 0 {
slog.Warn("could not extract GitHub username from Slack email",
"slack_user_id", slackUserID,
"email", email)
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(ctx, slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, nil)
}

// Get config for first org to extract domain and user overrides
cfg, exists := h.configManager.Config(workspaceOrgs[0])
if !exists {
return fmt.Errorf("no config for org: %s", workspaceOrgs[0])
}

// Update reverse mapping overrides from config
if len(cfg.Users) > 0 {
h.reverseMapping.SetOverrides(cfg.Users)
}

// Map Slack user to GitHub username
mapping, err := h.reverseMapping.LookupGitHub(ctx, slackClient.API(), slackUserID, workspaceOrgs[0], cfg.Global.EmailDomain)
if err != nil {
slog.Warn("failed to map Slack user to GitHub",
"slack_user_id", slackUserID,
"error", err)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, nil)
}

githubUsername := mapping.GitHubUsername

// Get GitHub client for first org (they all share the same app)
githubClient, ok := h.githubManager.ClientForOrg(workspaceOrgs[0])
if !ok {
Expand All @@ -130,14 +133,14 @@ func (h *HomeHandler) tryHandleAppHomeOpened(ctx context.Context, teamID, slackU
slog.Error("failed to fetch dashboard",
"github_user", githubUsername,
"error", err)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, mapping)
}

// Add workspace orgs to dashboard for UI display
dashboard.WorkspaceOrgs = workspaceOrgs

// Build Block Kit UI - use first org as primary
blocks := home.BuildBlocks(dashboard, workspaceOrgs[0])
// Build Block Kit UI - use first org as primary, include debug info
blocks := home.BuildBlocksWithDebug(dashboard, workspaceOrgs[0], mapping)

// Publish to Slack
if err := slackClient.PublishHomeView(ctx, slackUserID, blocks); err != nil {
Expand Down Expand Up @@ -177,14 +180,14 @@ func (h *HomeHandler) workspaceOrgs(teamID string) []string {
}

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

blocks := home.BuildBlocks(&home.Dashboard{
blocks := home.BuildBlocksWithDebug(&home.Dashboard{
IncomingPRs: nil,
OutgoingPRs: nil,
WorkspaceOrgs: []string{"your-org"},
}, "your-org")
}, "your-org", mapping)

return slackClient.PublishHomeView(ctx, slackUserID, blocks)
}
Loading
Loading