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
46 changes: 37 additions & 9 deletions bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,7 @@ func (b *Bot) connect(ctx context.Context) error {
}
errc := make(chan error, 1)
client := irc.NewClient(conn, irc.ClientConfig{Nick: b.Config.Identity.Nick, User: b.Config.Identity.User, Name: b.Config.Identity.Realname, Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
if b.Config.Identity.SASLUser != "" && b.Config.Identity.SASLPass != "" {
handleSASL(c, m, b.Config.Identity.SASLUser, b.Config.Identity.SASLPass, b.Log)
}
handleSASL(c, m, b.Config.Identity.SASLUser, b.Config.Identity.SASLPass, b.Log)
b.logIRCEvent(m)
if m.Command == "INVITE" {
b.handleInvite(m)
Expand Down Expand Up @@ -250,8 +248,10 @@ func (b *Bot) connect(ctx context.Context) error {
zap.String("server", b.Config.Server.Host),
zap.String("account", b.Config.Identity.SASLUser),
)
client.Write("CAP LS 302")
} else {
b.Log.Info("starting IRC capability negotiation", zap.String("network", b.Config.NetworkName), zap.String("server", b.Config.Server.Host))
}
client.Write("CAP LS 302")
go func() { errc <- client.Run() }()
select {
case <-ctx.Done():
Expand Down Expand Up @@ -462,17 +462,25 @@ func handleSASL(client *irc.Client, message *irc.Message, username, password str
subcommand := strings.ToUpper(message.Params[1])
switch subcommand {
case "LS":
if strings.Contains(strings.ToLower(message.Trailing()), "sasl") {
log.Info("server supports SASL")
client.Write("CAP REQ :sasl")
request := capabilityRequest(message.Trailing(), username != "" && password != "")
if request != "" {
if hasCapability(message.Trailing(), "sasl") {
log.Info("server supports SASL")
}
if hasCapability(message.Trailing(), "account-tag") {
log.Info("server supports account-tag")
}
client.Write("CAP REQ :" + request)
} else {
log.Warn("server did not advertise SASL")
log.Warn("server did not advertise requested IRC capabilities")
client.Write("CAP END")
}
case "ACK":
if strings.Contains(strings.ToLower(message.Trailing()), "sasl") {
if hasCapability(message.Trailing(), "sasl") && username != "" && password != "" {
log.Info("server acknowledged SASL capability")
client.Write("AUTHENTICATE PLAIN")
} else {
client.Write("CAP END")
}
}
case "AUTHENTICATE":
Expand All @@ -493,6 +501,26 @@ func handleSASL(client *irc.Client, message *irc.Message, username, password str
}
}

func capabilityRequest(advertised string, saslEnabled bool) string {
requested := make([]string, 0, 2)
if saslEnabled && hasCapability(advertised, "sasl") {
requested = append(requested, "sasl")
}
if hasCapability(advertised, "account-tag") {
requested = append(requested, "account-tag")
}
return strings.Join(requested, " ")
}

func hasCapability(advertised, wanted string) bool {
for _, capability := range strings.Fields(strings.ToLower(advertised)) {
if strings.SplitN(capability, "=", 2)[0] == strings.ToLower(wanted) {
return true
}
}
return false
}

func (b *Bot) shouldUseNickServFallback() bool {
return b.Config.Identity.NickServFallback && b.Config.Identity.SASLUser != "" && b.Config.Identity.SASLPass != ""
}
Expand Down
12 changes: 12 additions & 0 deletions bot/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,15 @@ func TestPrivateReloadIsOwnerOnly(t *testing.T) {
t.Fatalf("unexpected reload handler calls: %d", called)
}
}

func TestCapabilityRequestIncludesAccountTag(t *testing.T) {
if got := capabilityRequest("multi-prefix sasl account-tag away", true); got != "sasl account-tag" {
t.Fatalf("capabilityRequest with SASL = %q, want %q", got, "sasl account-tag")
}
if got := capabilityRequest("account-tag", false); got != "account-tag" {
t.Fatalf("capabilityRequest without SASL = %q, want %q", got, "account-tag")
}
if got := capabilityRequest("sasl", false); got != "" {
t.Fatalf("capabilityRequest without account tag = %q, want empty", got)
}
}
30 changes: 29 additions & 1 deletion plugins/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,19 @@ func (p *Ask) Handle(b *bot.Bot, m bot.Message) bool {
}
started := time.Now()
if rewritten, ok := p.rewriteWithConfig(ctx, question, source, cfg); ok {
if rewritten = cleanExternalText(rewritten); usableAskRewrite(rewritten) {
rewritten = cleanExternalText(rewritten)
if usableAskRewrite(rewritten) {
answer = rewritten
if b.Log != nil {
b.Log.Info("ask AI rewrite used", zap.String("provider", provider), zap.Duration("duration", time.Since(started)))
}
} else if b.Log != nil {
b.Log.Warn("ask AI rewrite rejected; using source summary",
zap.String("provider", provider),
zap.String("model", model),
zap.String("reason", askRewriteRejectionReason(rewritten)),
zap.Duration("duration", time.Since(started)),
)
}
} else if b.Log != nil {
b.Log.Warn("ask AI rewrite unavailable; using source summary", zap.String("provider", provider), zap.String("model", model), zap.Duration("duration", time.Since(started)))
Expand Down Expand Up @@ -251,6 +259,26 @@ func usableAskRewrite(answer string) bool {
return true
}

func askRewriteRejectionReason(answer string) string {
answer = strings.ToLower(strings.TrimSpace(answer))
if answer == "" {
return "empty_response"
}
for _, phrase := range []string{
"the user asks",
"the source does not",
"the source is",
"the answer should be",
"according to the source",
"not enough information in this source",
} {
if strings.Contains(answer, phrase) {
return "provider_meta_text"
}
}
return "unusable_response"
}

type askSource struct {
Title string
Summary string
Expand Down
16 changes: 16 additions & 0 deletions plugins/ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,19 @@ func TestUsableAskRewriteRejectsProviderMetaText(t *testing.T) {
t.Fatal("usableAskRewrite rejected a direct factual answer")
}
}

func TestAskRewriteRejectionReasonDoesNotExposeResponse(t *testing.T) {
tests := []struct {
answer string
want string
}{
{answer: "", want: "empty_response"},
{answer: "The user asks: what is Linux?", want: "provider_meta_text"},
{answer: "A response with no accepted structure", want: "unusable_response"},
}
for _, test := range tests {
if got := askRewriteRejectionReason(test.answer); got != test.want {
t.Errorf("askRewriteRejectionReason(%q) = %q, want %q", test.answer, got, test.want)
}
}
}