Skip to content

Commit b90259e

Browse files
authored
Merge pull request #69 from codeGROOVE-dev/reliable
Add reverse Slack->GitHub mapping for a better home experience
2 parents 9e5be0b + c73122d commit b90259e

File tree

10 files changed

+631
-42
lines changed

10 files changed

+631
-42
lines changed

cmd/server/main.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/codeGROOVE-dev/slacker/pkg/notify"
2424
"github.com/codeGROOVE-dev/slacker/pkg/slack"
2525
"github.com/codeGROOVE-dev/slacker/pkg/state"
26+
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
2627
"github.com/codeGROOVE-dev/sprinkler/pkg/client"
2728
"github.com/gorilla/mux"
2829
"golang.org/x/sync/errgroup"
@@ -244,8 +245,24 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi
244245
// Initialize event router for multi-workspace event handling.
245246
eventRouter := slack.NewEventRouter(slackManager)
246247

248+
// Initialize reverse user mapping service (Slack → GitHub)
249+
// Get GitHub token from one of the installations
250+
var githubToken string
251+
for _, org := range githubManager.AllOrgs() {
252+
if client, ok := githubManager.ClientForOrg(org); ok {
253+
githubToken = client.InstallationToken(ctx)
254+
break
255+
}
256+
}
257+
if githubToken == "" {
258+
slog.Warn("no GitHub installations found - reverse user mapping will not work")
259+
}
260+
// Pass nil for Slack client - it will be provided per-request in HomeHandler
261+
reverseMapping := usermapping.NewReverseService(nil, githubToken)
262+
slog.Info("initialized reverse user mapping service (Slack → GitHub)")
263+
247264
// Initialize home view handler
248-
homeHandler := slack.NewHomeHandler(slackManager, githubManager, configManager, stateStore)
265+
homeHandler := slack.NewHomeHandler(slackManager, githubManager, configManager, stateStore, reverseMapping)
249266
slackManager.SetHomeViewHandler(homeHandler.HandleAppHomeOpened)
250267

251268
// Initialize OAuth handler for Slack app installation.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.1
44

55
require (
66
github.com/codeGROOVE-dev/ds9 v0.6.0
7-
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9
7+
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928
88
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22
99
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4
1010
github.com/codeGROOVE-dev/retry v1.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
44
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
55
github.com/codeGROOVE-dev/ds9 v0.6.0 h1:JG7vBH17UAKaVoeQilrIvA1I0fg3iNbdUMBSDS7ixgI=
66
github.com/codeGROOVE-dev/ds9 v0.6.0/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
7-
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9 h1:eyWcEZd3xyLV2WxShoyKWakFyxQGvOSv89ponU3Ah0I=
8-
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251024133418-149270eb16a9/go.mod h1:4Hr2ySB8dcpeZqZq/7UbXdEJ/5RK9coYGHvW90ZfieE=
7+
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928 h1:fDiQ7GnN6tDUIrYqXKCmxcatFzZqr+Bp3aNBA0Q2AVk=
8+
github.com/codeGROOVE-dev/gh-mailto v0.0.0-20251030132316-7b86852c2928/go.mod h1:4Hr2ySB8dcpeZqZq/7UbXdEJ/5RK9coYGHvW90ZfieE=
99
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22 h1:gtN3rOc6YspO646BkcOxBhPjEqKUz+jl175jIqglfDg=
1010
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22/go.mod h1:KV+w19ubP32PxZPE1hOtlCpTaNpF0Bpb32w5djO8UTg=
1111
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4 h1:DSuoUwP3oyR4cHrX0cUh9c7CtYjXNIcyCmqpIwHilIU=

pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type RepoConfig struct {
5151
ReminderDMDelay int `yaml:"reminder_dm_delay"` // Minutes to wait before sending DM if user tagged in channel (0 = disabled)
5252
DailyReminders bool `yaml:"daily_reminders"`
5353
} `yaml:"global"`
54+
Users map[string]string `yaml:"users"` // GitHub username -> email address (for manual overrides)
5455
}
5556

5657
// configCacheEntry represents a cached configuration entry.

pkg/home/ui.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
910
"github.com/slack-go/slack"
1011
)
1112

@@ -226,3 +227,61 @@ func formatEnhancedPRBlock(pr *PR) slack.Block {
226227
nil,
227228
)
228229
}
230+
231+
// BuildBlocksWithDebug creates Slack Block Kit UI with debug information about user mapping.
232+
func BuildBlocksWithDebug(dashboard *Dashboard, primaryOrg string, mapping *usermapping.ReverseMapping) []slack.Block {
233+
// Build standard blocks first
234+
blocks := BuildBlocks(dashboard, primaryOrg)
235+
236+
// Add debug section if mapping info is available
237+
if mapping != nil {
238+
blocks = append(blocks,
239+
slack.NewDividerBlock(),
240+
slack.NewSectionBlock(
241+
slack.NewTextBlockObject("mrkdwn",
242+
fmt.Sprintf("🔍 *Debug Info*\n"+
243+
"GitHub: `@%s` • Mapped via: `%s` • Confidence: `%d%%`",
244+
mapping.GitHubUsername,
245+
mapping.MatchMethod,
246+
mapping.Confidence),
247+
false,
248+
false,
249+
),
250+
nil,
251+
nil,
252+
),
253+
)
254+
255+
// Add mapping guidance if confidence is low
256+
if mapping.Confidence < 80 {
257+
blocks = append(blocks,
258+
slack.NewContextBlock("",
259+
slack.NewTextBlockObject("mrkdwn",
260+
fmt.Sprintf("⚠️ Low confidence mapping. Add manual override to `slack.yaml`:\n```yaml\nusers:\n %s: %s\n```",
261+
mapping.GitHubUsername,
262+
mapping.SlackEmail),
263+
false,
264+
false,
265+
),
266+
),
267+
)
268+
}
269+
} else {
270+
// No mapping found - show error message
271+
blocks = append(blocks,
272+
slack.NewDividerBlock(),
273+
slack.NewSectionBlock(
274+
slack.NewTextBlockObject("mrkdwn",
275+
"❌ *Could not map Slack user to GitHub*\n"+
276+
"Add your mapping to `.codeGROOVE/slack.yaml`:\n```yaml\nusers:\n your-github-username: [email protected]\n```",
277+
false,
278+
false,
279+
),
280+
nil,
281+
nil,
282+
),
283+
)
284+
}
285+
286+
return blocks
287+
}

pkg/slack/home_handler.go

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ import (
1111
"github.com/codeGROOVE-dev/slacker/pkg/github"
1212
"github.com/codeGROOVE-dev/slacker/pkg/home"
1313
"github.com/codeGROOVE-dev/slacker/pkg/state"
14+
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
1415
gogithub "github.com/google/go-github/v50/github"
1516
)
1617

1718
// HomeHandler handles app_home_opened events for a workspace.
1819
type HomeHandler struct {
19-
slackManager *Manager
20-
githubManager *github.Manager
21-
configManager *config.Manager
22-
stateStore state.Store
20+
slackManager *Manager
21+
githubManager *github.Manager
22+
configManager *config.Manager
23+
stateStore state.Store
24+
reverseMapping *usermapping.ReverseService
2325
}
2426

2527
// NewHomeHandler creates a new home view handler.
@@ -28,12 +30,14 @@ func NewHomeHandler(
2830
githubManager *github.Manager,
2931
configManager *config.Manager,
3032
stateStore state.Store,
33+
reverseMapping *usermapping.ReverseService,
3134
) *HomeHandler {
3235
return &HomeHandler{
33-
slackManager: slackManager,
34-
githubManager: githubManager,
35-
configManager: configManager,
36-
stateStore: stateStore,
36+
slackManager: slackManager,
37+
githubManager: githubManager,
38+
configManager: configManager,
39+
stateStore: stateStore,
40+
reverseMapping: reverseMapping,
3741
}
3842
}
3943

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

80-
// Get Slack user info to extract email
81-
slackUser, err := slackClient.API().GetUserInfo(slackUserID)
82-
if err != nil {
83-
// Don't mask invalid_auth errors - let them propagate for retry logic
84-
if strings.Contains(err.Error(), "invalid_auth") {
85-
return fmt.Errorf("failed to get Slack user info: %w", err)
86-
}
87-
slog.Warn("failed to get Slack user info", "user_id", slackUserID, "error", err)
88-
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
89-
}
90-
91-
// Extract GitHub username from email (simple heuristic: part before @)
92-
// Works for "[email protected]" -> "username"
93-
email := slackUser.Profile.Email
94-
atIndex := strings.IndexByte(email, '@')
95-
if atIndex <= 0 {
96-
slog.Warn("could not extract GitHub username from Slack email",
97-
"slack_user_id", slackUserID,
98-
"email", email)
99-
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
100-
}
101-
githubUsername := email[:atIndex]
102-
10384
// Get all orgs for this workspace
10485
workspaceOrgs := h.workspaceOrgs(teamID)
10586
if len(workspaceOrgs) == 0 {
10687
slog.Warn("no workspace orgs found", "team_id", teamID)
107-
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
88+
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, nil)
89+
}
90+
91+
// Get config for first org to extract domain and user overrides
92+
cfg, exists := h.configManager.Config(workspaceOrgs[0])
93+
if !exists {
94+
return fmt.Errorf("no config for org: %s", workspaceOrgs[0])
10895
}
10996

97+
// Update reverse mapping overrides from config
98+
if len(cfg.Users) > 0 {
99+
h.reverseMapping.SetOverrides(cfg.Users)
100+
}
101+
102+
// Map Slack user to GitHub username
103+
mapping, err := h.reverseMapping.LookupGitHub(ctx, slackClient.API(), slackUserID, workspaceOrgs[0], cfg.Global.EmailDomain)
104+
if err != nil {
105+
slog.Warn("failed to map Slack user to GitHub",
106+
"slack_user_id", slackUserID,
107+
"error", err)
108+
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, nil)
109+
}
110+
111+
githubUsername := mapping.GitHubUsername
112+
110113
// Get GitHub client for first org (they all share the same app)
111114
githubClient, ok := h.githubManager.ClientForOrg(workspaceOrgs[0])
112115
if !ok {
@@ -130,14 +133,14 @@ func (h *HomeHandler) tryHandleAppHomeOpened(ctx context.Context, teamID, slackU
130133
slog.Error("failed to fetch dashboard",
131134
"github_user", githubUsername,
132135
"error", err)
133-
return h.publishPlaceholderHome(ctx, slackClient, slackUserID)
136+
return h.publishPlaceholderHome(ctx, slackClient, slackUserID, mapping)
134137
}
135138

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

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

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

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

183-
blocks := home.BuildBlocks(&home.Dashboard{
186+
blocks := home.BuildBlocksWithDebug(&home.Dashboard{
184187
IncomingPRs: nil,
185188
OutgoingPRs: nil,
186189
WorkspaceOrgs: []string{"your-org"},
187-
}, "your-org")
190+
}, "your-org", mapping)
188191

189192
return slackClient.PublishHomeView(ctx, slackUserID, blocks)
190193
}

0 commit comments

Comments
 (0)