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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ msgs, _ := c.Messages().Read(ctx, "", 20, "") // oldest-first
## Notes

Channel-scoped ops accept `""` for the configured default channel; guild-scoped
ops accept `""` for the bot's sole guild. Nothing is read from the environment —
pass the token explicitly. Without one, `Enabled()` is false and every call
returns `ErrDisabled`. `WithHTTPClient` overrides the default 15s client.
ops accept `""` for the bot's sole guild — or for the guild pinned by
`WithGuild`, which a bot in several servers needs. Nothing is read from the
environment — pass the token explicitly. Without one, `Enabled()` is false and
every call returns `ErrDisabled`. `WithHTTPClient` overrides the default 15s
client.

Slash commands: `NewCommand(...).With(dctl.String(...), dctl.Sub(...))` builds a
typed command; `c.Interactions().Registry()` owns `Add`, `Sync` (diff against
Expand Down
13 changes: 12 additions & 1 deletion dctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,22 @@ type ClientOption func(*clientConfig)

type clientConfig struct {
httpClient *http.Client
guild string
}

// WithHTTPClient overrides the default 15s-timeout HTTP client.
func WithHTTPClient(h *http.Client) ClientOption {
return func(c *clientConfig) { c.httpClient = h }
}

// WithGuild pins the guild that guild-scoped ops target when no explicit id is
// passed. Without it they fall back to Guilds().Sole, which errors when the bot
// is in more than one server — a bot invited to a second guild would otherwise
// lose command registration entirely.
func WithGuild(id string) ClientOption {
return func(c *clientConfig) { c.guild = id }
}

// New builds a Client. token is the bot token (kept in memory only). defaultChannel
// is the channel that message ops target when no explicit channel id is passed.
func New(token, defaultChannel string, opts ...ClientOption) *Client {
Expand All @@ -39,7 +48,9 @@ func New(token, defaultChannel string, opts ...ClientOption) *Client {
topts = append(topts, transport.WithHTTPClient(cfg.httpClient))
}
rt := transport.NewHTTP(token, topts...)
return newWith(rt, defaultChannel)
c := newWith(rt, defaultChannel)
c.def.guild = cfg.guild
return c
}

// newWith wires a Client around an arbitrary Doer (used by tests with a stub).
Expand Down
6 changes: 6 additions & 0 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var ErrNoChannel = errors.New("dctl: no channel (DISCORD_CHANNEL_ID or --channel
type defaults struct {
rt transport.Doer
channel string
guild string
guilds *Guilds

// appID and the sole-guild id are resolved independently via one network
Expand All @@ -43,6 +44,11 @@ func (d *defaults) resolveGuild(ctx context.Context, guildID string) (string, er
if guildID != "" {
return guildID, nil
}
// A configured guild (WithGuild) settles it without a network call and
// without the mono-server assumption Sole makes.
if d.guild != "" {
return d.guild, nil
}
d.muGuild.Lock()
defer d.muGuild.Unlock()
if d.soleGuid != "" {
Expand Down
24 changes: 24 additions & 0 deletions defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ func TestDefaultsResolveChannelErrorsWhenNone(t *testing.T) {
}
}

// A configured guild must win over the sole-guild lookup: a bot in several
// servers has no sole guild, and the lookup would fail the call outright.
func TestResolveGuildPrefersConfiguredGuild(t *testing.T) {
s := transport.NewStub().Reply(`[{"id":"g1","name":"a"},{"id":"g2","name":"b"}]`)
d := &defaults{guild: "g2", guilds: &Guilds{rt: s}}
got, err := d.resolveGuild(context.Background(), "")
if err != nil || got != "g2" {
t.Fatalf("got %q, %v", got, err)
}
if _, err := (&defaults{guilds: &Guilds{rt: s}}).resolveGuild(context.Background(), ""); err == nil {
t.Fatal("without a configured guild a multi-server bot should still fail")
}
}

func TestWithGuildConfiguresDefault(t *testing.T) {
c := New("token", "chan", WithGuild("g7"))
if c.def.guild != "g7" {
t.Fatalf("def.guild = %q, want g7", c.def.guild)
}
if plain := New("token", "chan"); plain.def.guild != "" {
t.Fatalf("def.guild = %q, want empty without WithGuild", plain.def.guild)
}
}

func TestResolveGuildUsesSoleGuild(t *testing.T) {
s := transport.NewStub().Reply(`[{"id":"g1","name":"srv"}]`)
d := &defaults{guilds: &Guilds{rt: s}}
Expand Down
Loading