all repos — paprika @ 25de5b6c6af4b418eb4cb6ef74eb26540f50ddd6

go rewrite of taigabot

Contextual Errors

This set of changes removes all the old CTCP code in favor of contextual
errors. The problem is, it also exposes a large amount of defects in
current prefix matching approaches that we need to discuss fix.

Error Types:

  * NoReply - DO NOT REPLY; continue matching prefixes.
  * ReplyT  - A set of flags that allow customizing how to send a reply.
Anthony DeDominic adedomin@gmail.com
Sun, 21 Nov 2021 19:34:12 -0500
commit

25de5b6c6af4b418eb4cb6ef74eb26540f50ddd6

parent

3a6f4cb1fc53b08fb810de6293a22718bb8ce9ef

M config/config.goconfig/config.go

@@ -1,7 +1,6 @@

package config import ( - "fmt" "io" "log" "os"
M config/confpath.goconfig/confpath.go

@@ -7,7 +7,7 @@ "os"

"path" ) -//go:embed example.yaml +//go:embed example.yml var exampleConfig []byte func configPaths() []string {
M main.gomain.go

@@ -1,7 +1,7 @@

package main import ( - "fmt" + "errors" "log" "net" "strings"

@@ -12,90 +12,37 @@ "git.icyphox.sh/paprika/plugins"

"gopkg.in/irc.v3" ) -// A simple check func to find out if an incoming irc message -// is supposed to be a CTCP message -func isCTCPmessage(m *irc.Message) bool { - // CTCP messages are identified by their first character byte - // being equal to 0x01. - return m.Params[1][0] == 0x01 -} - -// CTCP responses are sent with a NOTICE, instead of a PRIVMSG -func sendCTCPResponse(c *irc.Client, m *irc.Message, command string, message string) { - c.WriteMessage(&irc.Message{ - Command: "NOTICE", - Params: []string{ - m.Prefix.Name, - fmt.Sprintf("\x01%s %s\x01", command, message), - }, - }) -} - -// for future use. Perhaps move all of this CTCP stuff out another file? -func sendCTCPRequest(c *irc.Client, m *irc.Message, command string, message string) { - c.WriteMessage(&irc.Message{ - Command: "PRIVMSG", - Params: []string{ - m.Prefix.Name, - fmt.Sprintf("\x01%s %s\x01", command, message), - }, - }) -} - -func handleCTCPMessage(c *irc.Client, m *irc.Message) { - - // Refer to for further commands to implement and as a general - // guide on how CTCP works: - // https://tools.ietf.org/id/draft-oakley-irc-ctcp-01.html - - var ctcpCommand = m.Params[1] // var might be named wrong - - // start of the main if/else tree for CTCP checking. idk why, but - // for some reason a straight switch/case comparison simply did not work - if strings.Contains(ctcpCommand, "VERSION") { - sendCTCPResponse(c, m, "VERSION", "Paprika v0.0.1") - } else if strings.Contains(ctcpCommand, "PING") { - // lotsa ugly string processing here, but w/e - - // split the incoming ping by word, strip out the first word (the command) - output := strings.Split(m.Params[1], " ")[1:] - outputStr := strings.Join(output, " ") // re-join - - // send response while stripping out the last char, - // somehwere deep in the wirting a random char gets added - // to the start and end of the incomming message. - // the first char gets stripped out along with the command word. - // the last char gets stripped out here. - sendCTCPResponse(c, m, "PING", outputStr[0:len(outputStr)-1]) - } else if strings.Contains(ctcpCommand, "CLIENTINFO") { - - // UPDATE THIS WITH ANY NEW CTCP COMMAND YOU IMPLEMENT - sendCTCPResponse( - c, m, - "CLIENTINFO", - "VERSION PING", - ) - } -} - func handleChatMessage(c *irc.Client, m *irc.Message) { response, err := plugins.ProcessTrigger(m) - if err != nil { - log.Printf("error: %v", err) - } + split := strings.Split(response, "\n") - // split the plugin output by it's newlines, send every line - // as a separate PRIVMSG - split := strings.Split(response, "\n") + if plugins.IsReplyT(err) { + r := err.(*plugins.ReplyT) + r.ApplyFlags(m) - for _, line := range split { - c.WriteMessage(&irc.Message{ - Command: "PRIVMSG", - Params: []string{ - m.Params[0], - line, - }, - }) + for _, line := range split { + c.WriteMessage(&irc.Message{ + Command: m.Command, + Params: []string { + m.Params[0], + line, + }, + }) + } + } else if err != nil { + if !errors.Is(err, plugins.NoReply) { + log.Printf("error: %v", err) + } + } else { + for _, line := range split { + c.WriteMessage(&irc.Message{ + Command: "PRIVMSG", + Params: []string{ + m.Params[0], + line, + }, + }) + } } }

@@ -104,11 +51,7 @@ switch m.Command {

case "001": c.Write(config.SplitChannelList(config.C.Channels)) case "PRIVMSG": - if isCTCPmessage(m) { - handleCTCPMessage(c, m) - } else { - handleChatMessage(c, m) - } + handleChatMessage(c, m) } }
A plugins/ctcp.go

@@ -0,0 +1,27 @@

+package plugins + +import ( + "strings" + + "gopkg.in/irc.v3" +) + +type Ctcp struct{} + +func init() { + Register(Ctcp{}) +} + +func (Ctcp) Triggers() []string { + return []string{"\x01VERSION\x01", "\x01PING"} +} + +func (Ctcp) Execute(m *irc.Message) (string, error) { + msg := m.Trailing() + if msg == "\x01VERSION\x01" { + return "\x01VERSION git.icyphox.sh/paprika\x01", NewReplyT(Notice | DirectMessage) + } else if strings.HasPrefix(msg, "\x01PING") { + return msg, NewReplyT(Notice | DirectMessage) + } + return "\x01INVAL\x01", NewReplyT(Notice | DirectMessage) +}
M plugins/plugins.goplugins/plugins.go

@@ -1,6 +1,8 @@

package plugins import ( + "errors" + "fmt" "strings" "gopkg.in/irc.v3"

@@ -19,21 +21,79 @@ Plugins[t] = p

} } +// This Error is used to signal a NAK so other plugins +// can attempt to parse the result +// This is useful for broad prefix matching or future regexp +// functions, other special errors may need defining +// to determin priority or to "concatenate" output. +var NoReply = errors.New("No Reply") + +type Flag int + +type ReplyT struct { + Flags Flag +} + +const ( + Notice Flag = 1 << iota + DirectMessage +) + +var AllFlags = [...]Flag{Notice, DirectMessage} + +func (f Flag) String() string { + switch f { + case Notice: + return "Notice" + case DirectMessage: + return "DM" + default: + return fmt.Sprintf("Invalid:%d", f) + } +} + +func NewReplyT(flags Flag) *ReplyT { + return &ReplyT{Flags: flags} +} + +func (r *ReplyT) ApplyFlags(m *irc.Message) { + for _, flag := range AllFlags { + if r.Flags & flag != 0 { + switch flag { + case Notice: + m.Command = "NOTICE" + case DirectMessage: + m.Params[0] = m.Name + } + } + } +} + +func (r *ReplyT) Error() string { + var reply strings.Builder + for _, flag := range AllFlags { + if r.Flags & flag != 0 { + reply.WriteString(flag.String()) + } + } + return reply.String() +} + +func IsReplyT(e error) bool { + r := &ReplyT{} + return errors.As(e, &r) +} + // Checks for triggers in a message and executes its // corresponding plugin, returning the response/error. func ProcessTrigger(m *irc.Message) (string, error) { - var ( - response string - err error - ) for trigger, plugin := range Plugins { if strings.HasPrefix(m.Trailing(), trigger) { - response, err = plugin.Execute(m) - if err != nil { - return "", err + response, err := plugin.Execute(m) + if !errors.Is(err, NoReply) { + return response, err } - return response, nil } } - return "", nil + return "", NoReply // No plugin matched, so we need to Ignore. }
M plugins/weather.goplugins/weather.go

@@ -24,6 +24,19 @@ }

func (Weather) Execute(m *irc.Message) (string, error) { parsed := strings.SplitN(m.Trailing(), " ", 2) + + // TODO: AAAAAAAAAAAAAAAAAAAAAAAAAAA + found := false + for _, v := range (Weather{}).Triggers() { + if parsed[0] == v { + found = true + break + } + } + if !found { + return "", NoReply + } + var loc string if len(parsed) != 2 { var err error