Skip to content

Commit 203e614

Browse files
committed
fix(resolve): fall back to channel when bare name fails user resolution
Bare names like "stand-up" were always assumed to be users by LooksLikeUser(), causing "user not found" when the name was actually a channel. Add ResolveTarget() that tries user (DM) first for bare names, then falls back to channel resolution before erroring. Explicit prefixes (#channel, @user) remain unambiguous with no fallback.
1 parent f44f4d9 commit 203e614

3 files changed

Lines changed: 54 additions & 24 deletions

File tree

internal/api/resolve.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,52 @@ func LooksLikeUser(target string) bool {
164164
return true
165165
}
166166

167+
// ResolveTarget resolves a bare name to a channel ID, trying the most likely
168+
// path first based on the name's syntax, then falling back to the other.
169+
//
170+
// Explicit prefixes are unambiguous:
171+
//
172+
// #general → channel only (no fallback)
173+
// @alice → DM only (no fallback)
174+
//
175+
// Bare names (e.g. "stand-up", "herman") are ambiguous — the name could be
176+
// either a channel or a user. We try the heuristic-preferred path first
177+
// (user for bare names) and fall back to the other on failure.
178+
//
179+
// Returns the resolved channel ID and whether the target resolved as a DM.
180+
func (r *Resolver) ResolveTarget(target string) (channelID string, isDM bool, err error) {
181+
// Explicit #channel — no ambiguity
182+
if strings.HasPrefix(target, "#") {
183+
id, err := r.ResolveChannel(target)
184+
return id, false, err
185+
}
186+
187+
// Explicit @user or user ID — no ambiguity
188+
if strings.HasPrefix(target, "@") || isUserID(target) {
189+
id, err := r.ResolveDM(target)
190+
return id, true, err
191+
}
192+
193+
// Explicit channel ID — no ambiguity
194+
if isChannelID(target) {
195+
return target, false, nil
196+
}
197+
198+
// Bare name — ambiguous. Try user (DM) first, fall back to channel.
199+
id, dmErr := r.ResolveDM(target)
200+
if dmErr == nil {
201+
return id, true, nil
202+
}
203+
204+
id, chErr := r.ResolveChannel(target)
205+
if chErr == nil {
206+
return id, false, nil
207+
}
208+
209+
// Both failed — return a combined error
210+
return "", false, fmt.Errorf("could not resolve %q as a user or channel", target)
211+
}
212+
167213
func (r *Resolver) loadUsers() error {
168214
r.mu.Lock()
169215
if r.usersLoaded {

pkg/cmd/file/upload.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,10 @@ func uploadRun(opts *uploadOptions) error {
102102
}
103103
}
104104

105-
// Resolve channel/DM
105+
// Resolve channel/DM — ResolveTarget handles bare names by trying
106+
// user (DM) first, then falling back to channel.
106107
resolver := api.NewResolver(client.Slack)
107-
var channelID string
108-
if api.LooksLikeUser(opts.channel) {
109-
channelID, err = resolver.ResolveDM(opts.channel)
110-
} else {
111-
channelID, err = resolver.ResolveChannel(opts.channel)
112-
}
108+
channelID, _, err := resolver.ResolveTarget(opts.channel)
113109

114110
// If resolution fails with missing scope, try the other token
115111
if err != nil && api.IsMissingScopeError(err) {
@@ -120,11 +116,7 @@ func uploadRun(opts *uploadOptions) error {
120116
if altErr == nil {
121117
fmt.Fprintf(ios.ErrOut, "%s Retrying with alternate token\n", cs.Yellow("!"))
122118
altResolver := api.NewResolver(altClient.Slack)
123-
if api.LooksLikeUser(opts.channel) {
124-
channelID, err = altResolver.ResolveDM(opts.channel)
125-
} else {
126-
channelID, err = altResolver.ResolveChannel(opts.channel)
127-
}
119+
channelID, _, err = altResolver.ResolveTarget(opts.channel)
128120
if err == nil {
129121
client = altClient
130122
}

pkg/cmd/message/send.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,9 @@ func sendRun(opts *sendOptions) error {
108108

109109
resolver := api.NewResolver(client.Slack)
110110

111-
// Resolve target to a channel ID — auto-detect DMs vs channels
112-
var channelID string
113-
if api.LooksLikeUser(opts.channel) {
114-
channelID, err = resolver.ResolveDM(opts.channel)
115-
} else {
116-
channelID, err = resolver.ResolveChannel(opts.channel)
117-
}
111+
// Resolve target to a channel ID — auto-detect DMs vs channels.
112+
// ResolveTarget handles bare names by trying user (DM) first, then channel.
113+
channelID, _, err := resolver.ResolveTarget(opts.channel)
118114

119115
// Bot-fallback: if resolution failed with missing_scope on the user token,
120116
// retry with the bot client (which typically has channels:read).
@@ -123,11 +119,7 @@ func sendRun(opts *sendOptions) error {
123119
if botErr == nil {
124120
fmt.Fprintf(ios.ErrOut, "%s User token missing scope for channel lookup — falling back to bot token\n", cs.Yellow("!"))
125121
botResolver := api.NewResolver(botClient.Slack)
126-
if api.LooksLikeUser(opts.channel) {
127-
channelID, err = botResolver.ResolveDM(opts.channel)
128-
} else {
129-
channelID, err = botResolver.ResolveChannel(opts.channel)
130-
}
122+
channelID, _, err = botResolver.ResolveTarget(opts.channel)
131123
}
132124
}
133125

0 commit comments

Comments
 (0)