source: code/trunk/irc.go@ 140

Last change on this file since 140 was 139, checked in by delthas, 5 years ago

Add MODE arguments support

  • Add RPL_ISUPPORT support with CHANMODES, CHANTYPES, PREFIX parsing
  • Add support for channel mode state with mode arguments
  • Add upstream support for RPL_UMODEIS, RPL_CHANNELMODEIS
  • Request channel MODE on upstream channel JOIN
  • Use sane default channel mode and channel mode types
File size: 4.4 KB
RevLine 
[98]1package soju
[20]2
3import (
4 "fmt"
5 "strings"
[43]6
7 "gopkg.in/irc.v3"
[20]8)
9
10const (
[108]11 rpl_statsping = "246"
12 rpl_localusers = "265"
13 rpl_globalusers = "266"
14 rpl_topicwhotime = "333"
15 err_invalidcapcmd = "410"
[20]16)
17
[139]18type userModes string
[20]19
[139]20func (ms userModes) Has(c byte) bool {
[20]21 return strings.IndexByte(string(ms), c) >= 0
22}
23
[139]24func (ms *userModes) Add(c byte) {
[20]25 if !ms.Has(c) {
[139]26 *ms += userModes(c)
[20]27 }
28}
29
[139]30func (ms *userModes) Del(c byte) {
[20]31 i := strings.IndexByte(string(*ms), c)
32 if i >= 0 {
33 *ms = (*ms)[:i] + (*ms)[i+1:]
34 }
35}
36
[139]37func (ms *userModes) Apply(s string) error {
[20]38 var plusMinus byte
39 for i := 0; i < len(s); i++ {
40 switch c := s[i]; c {
41 case '+', '-':
42 plusMinus = c
43 default:
44 switch plusMinus {
45 case '+':
46 ms.Add(c)
47 case '-':
48 ms.Del(c)
49 default:
50 return fmt.Errorf("malformed modestring %q: missing plus/minus", s)
51 }
52 }
53 }
54 return nil
55}
56
[139]57type channelModeType byte
58
59// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
60const (
61 // modes that add or remove an address to or from a list
62 modeTypeA channelModeType = iota
63 // modes that change a setting on a channel, and must always have a parameter
64 modeTypeB
65 // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
66 modeTypeC
67 // modes that change a setting on a channel, and must not have a parameter
68 modeTypeD
69)
70
71var stdChannelModes = map[byte]channelModeType{
72 'b': modeTypeA, // ban list
73 'e': modeTypeA, // ban exception list
74 'I': modeTypeA, // invite exception list
75 'k': modeTypeB, // channel key
76 'l': modeTypeC, // channel user limit
77 'i': modeTypeD, // channel is invite-only
78 'm': modeTypeD, // channel is moderated
79 'n': modeTypeD, // channel has no external messages
80 's': modeTypeD, // channel is secret
81 't': modeTypeD, // channel has protected topic
82}
83
84type channelModes map[byte]string
85
86func (cm channelModes) Apply(modeTypes map[byte]channelModeType, modeStr string, arguments ...string) error {
87 nextArgument := 0
88 var plusMinus byte
89 for i := 0; i < len(modeStr); i++ {
90 mode := modeStr[i]
91 if mode == '+' || mode == '-' {
92 plusMinus = mode
93 continue
94 }
95 if plusMinus != '+' && plusMinus != '-' {
96 return fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
97 }
98
99 mt, ok := modeTypes[mode]
100 if !ok {
101 continue
102 }
103 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
104 if plusMinus == '+' {
105 var argument string
106 // some sentitive arguments (such as channel keys) can be omitted for privacy
107 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
108 if nextArgument < len(arguments) {
109 argument = arguments[nextArgument]
110 }
111 cm[mode] = argument
112 } else {
113 delete(cm, mode)
114 }
115 nextArgument++
116 } else if mt == modeTypeC || mt == modeTypeD {
117 if plusMinus == '+' {
118 cm[mode] = ""
119 } else {
120 delete(cm, mode)
121 }
122 }
123 }
124 return nil
125}
126
127func (cm channelModes) Format() (modeString string, parameters []string) {
128 var modesWithValues strings.Builder
129 var modesWithoutValues strings.Builder
130 parameters = make([]string, 0, 16)
131 for mode, value := range cm {
132 if value != "" {
133 modesWithValues.WriteString(string(mode))
134 parameters = append(parameters, value)
135 } else {
136 modesWithoutValues.WriteString(string(mode))
137 }
138 }
139 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
140 return
141}
142
143const stdChannelTypes = "#&+!"
144
[20]145type channelStatus byte
146
147const (
148 channelPublic channelStatus = '='
149 channelSecret channelStatus = '@'
150 channelPrivate channelStatus = '*'
151)
152
153func parseChannelStatus(s string) (channelStatus, error) {
154 if len(s) > 1 {
155 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
156 }
157 switch cs := channelStatus(s[0]); cs {
158 case channelPublic, channelSecret, channelPrivate:
159 return cs, nil
160 default:
161 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
162 }
163}
164
[139]165type membership struct {
166 Mode byte
167 Prefix byte
168}
[20]169
[139]170var stdMemberships = []membership{
171 {'q', '~'}, // founder
172 {'a', '&'}, // protected
173 {'o', '@'}, // operator
174 {'h', '%'}, // halfop
175 {'v', '+'}, // voice
176}
[20]177
[139]178func (m *membership) String() string {
179 if m == nil {
[128]180 return ""
181 }
[139]182 return string(m.Prefix)
[128]183}
184
[43]185func parseMessageParams(msg *irc.Message, out ...*string) error {
186 if len(msg.Params) < len(out) {
187 return newNeedMoreParamsError(msg.Command)
188 }
189 for i := range out {
190 if out[i] != nil {
191 *out[i] = msg.Params[i]
192 }
193 }
194 return nil
195}
Note: See TracBrowser for help on using the repository browser.