all repos — paprika @ 5d7c1b2e1cb095849165e6c4b2c8ec469886b5a1

go rewrite of taigabot

plugins/plugins.go (view raw)

  1package plugins
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"strings"
  7
  8	"gopkg.in/irc.v3"
  9)
 10
 11type Plugin interface {
 12	Triggers() []string
 13	Execute(m *irc.Message) (string, error)
 14}
 15
 16var Plugins = make(map[string]Plugin)
 17
 18func Register(p Plugin) {
 19	for _, t := range p.Triggers() {
 20		Plugins[t] = p
 21	}
 22}
 23
 24// This Error is used to signal a NAK so other plugins
 25// can attempt to parse the result
 26// This is useful for broad prefix matching or future regexp
 27// functions, other special errors may need defining
 28// to determin priority or to "concatenate" output.
 29var NoReply = errors.New("No Reply")
 30
 31// This error indicates we are sending a NOTICE instead of a PRIVMSG
 32var IsNotice = errors.New("Is Notice")
 33
 34// This means the string(s) we are returning are raw IRC commands
 35// that need to be written verbatim.
 36var IsRaw = errors.New("Is Raw")
 37
 38// This error indicates that the message is a NOTICE, along with a
 39// recepient.
 40type IsPrivateNotice struct {
 41	To string
 42}
 43
 44func (e *IsPrivateNotice) Error() string {
 45	return fmt.Sprintf("Is Private Notice: %s", e.To)
 46}
 47
 48// Due to racey nature of the handler in main.go being invoked as a goroutine,
 49// it's hard to have race free way of building correct state of the IRC world.
 50// This is a temporary (lol) hack to check if the PRIVMSG target looks like a
 51// IRC nickname. We assume that any IRC nickname target would be us, unless
 52// the IRCd we are on is neurotic
 53//
 54// Normally one would use the ISUPPORT details to learn what prefixes are used
 55// on the network's valid channel names.
 56//
 57// E.G. ISUPPORT ... CHANTYPES=# ... where # would be the only valid channel name
 58// allowed on the IRCd.
 59func unlikelyDirectMessage(target string) bool {
 60	if len(target) < 1 {
 61		panic("Conformity Error, IRCd sent us a PRIVMSG with an empty target and message.")
 62	}
 63
 64	sym := target[0] // we only care about the byte (ASCII)
 65	return likelyInvalidNickChr(sym)
 66}
 67
 68func likelyInvalidNickChr(sym byte) bool {
 69	// Is one of: !"#$%&'()*+,_./
 70	// or one of: ;<=>?@
 71	// If your IRCd uses symbols outside of this range,
 72	// god help us.
 73	//
 74	// ALSO NOTE: RFC1459 defines a "nick" as
 75	// <nick> ::= <letter> { <letter> | <number> | <special> }
 76	// But I have seen some networks that allow special/number as the first letter.
 77	return sym > 32 /* SPACE */ && sym < 48 /* 0 */ ||
 78		sym > 58 /* : */ && sym < 65 /* A */ ||
 79		sym == 126 /* ~ */
 80}
 81
 82func likelyInvalidNick(nick string) bool {
 83	for i := 0; i < len(nick); i++ {
 84		if likelyInvalidNickChr(nick[i]) {
 85			return true
 86		}
 87	}
 88	return false
 89}
 90
 91// Checks for triggers in a message and executes its
 92// corresponding plugin, returning the response/error.
 93func ProcessTrigger(m *irc.Message) (string, error) {
 94	if !unlikelyDirectMessage(m.Params[0]) {
 95		m.Params[0] = m.Name
 96	}
 97
 98	for trigger, plugin := range Plugins {
 99		if strings.HasPrefix(m.Trailing(), trigger) {
100			response, err := plugin.Execute(m)
101			if !errors.Is(err, NoReply) {
102				return response, err
103			}
104		}
105	}
106	return "", NoReply // No plugin matched, so we need to Ignore.
107}