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}