source: code/trunk/irc.go@ 296

Last change on this file since 296 was 293, checked in by delthas, 5 years ago

Fix parsing MODE messages by updating channel memberships

Previously, we only considered channel modes in the modes of a MODE
messages, which means channel membership changes were ignored. This
resulted in bugs where users channel memberships would not be properly
updated and cached with wrong values. Further, mode arguments
representing entities were not properly marshaled.

This adds support for correctly parsing and updating channel memberships
when processing MODE messages. Mode arguments corresponding to channel
memberships updates are now also properly marshaled.

MODE messages can't be easily sent from history because marshaling these
messages require knowing about the upstream available channel types and
channel membership types, which is currently only possible when
connected. For now this is not an issue since we do not send MODE
messages in history.

File size: 6.9 KB
Line 
1package soju
2
3import (
4 "fmt"
5 "strings"
6
7 "gopkg.in/irc.v3"
8)
9
10const (
11 rpl_statsping = "246"
12 rpl_localusers = "265"
13 rpl_globalusers = "266"
14 rpl_creationtime = "329"
15 rpl_topicwhotime = "333"
16 err_invalidcapcmd = "410"
17)
18
19type userModes string
20
21func (ms userModes) Has(c byte) bool {
22 return strings.IndexByte(string(ms), c) >= 0
23}
24
25func (ms *userModes) Add(c byte) {
26 if !ms.Has(c) {
27 *ms += userModes(c)
28 }
29}
30
31func (ms *userModes) Del(c byte) {
32 i := strings.IndexByte(string(*ms), c)
33 if i >= 0 {
34 *ms = (*ms)[:i] + (*ms)[i+1:]
35 }
36}
37
38func (ms *userModes) Apply(s string) error {
39 var plusMinus byte
40 for i := 0; i < len(s); i++ {
41 switch c := s[i]; c {
42 case '+', '-':
43 plusMinus = c
44 default:
45 switch plusMinus {
46 case '+':
47 ms.Add(c)
48 case '-':
49 ms.Del(c)
50 default:
51 return fmt.Errorf("malformed modestring %q: missing plus/minus", s)
52 }
53 }
54 }
55 return nil
56}
57
58type channelModeType byte
59
60// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
61const (
62 // modes that add or remove an address to or from a list
63 modeTypeA channelModeType = iota
64 // modes that change a setting on a channel, and must always have a parameter
65 modeTypeB
66 // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
67 modeTypeC
68 // modes that change a setting on a channel, and must not have a parameter
69 modeTypeD
70)
71
72var stdChannelModes = map[byte]channelModeType{
73 'b': modeTypeA, // ban list
74 'e': modeTypeA, // ban exception list
75 'I': modeTypeA, // invite exception list
76 'k': modeTypeB, // channel key
77 'l': modeTypeC, // channel user limit
78 'i': modeTypeD, // channel is invite-only
79 'm': modeTypeD, // channel is moderated
80 'n': modeTypeD, // channel has no external messages
81 's': modeTypeD, // channel is secret
82 't': modeTypeD, // channel has protected topic
83}
84
85type channelModes map[byte]string
86
87// applyChannelModes parses a mode string and mode arguments from a MODE message,
88// and applies the corresponding channel mode and user membership changes on that channel.
89//
90// If ch.modes is nil, channel modes are not updated.
91//
92// needMarshaling is a list of indexes of mode arguments that represent entities
93// that must be marshaled when sent downstream.
94func applyChannelModes(ch *upstreamChannel, modeStr string, arguments []string) (needMarshaling map[int]struct{}, err error) {
95 needMarshaling = make(map[int]struct{}, len(arguments))
96 nextArgument := 0
97 var plusMinus byte
98outer:
99 for i := 0; i < len(modeStr); i++ {
100 mode := modeStr[i]
101 if mode == '+' || mode == '-' {
102 plusMinus = mode
103 continue
104 }
105 if plusMinus != '+' && plusMinus != '-' {
106 return nil, fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
107 }
108
109 for _, membership := range ch.conn.availableMemberships {
110 if membership.Mode == mode {
111 if nextArgument >= len(arguments) {
112 return nil, fmt.Errorf("malformed modestring %q: missing mode argument for %c%c", modeStr, plusMinus, mode)
113 }
114 member := arguments[nextArgument]
115 if _, ok := ch.Members[member]; ok {
116 if plusMinus == '+' {
117 ch.Members[member].Add(ch.conn.availableMemberships, membership)
118 } else {
119 // TODO: for upstreams without multi-prefix, query the user modes again
120 ch.Members[member].Remove(membership)
121 }
122 }
123 needMarshaling[nextArgument] = struct{}{}
124 nextArgument++
125 continue outer
126 }
127 }
128
129 mt, ok := ch.conn.availableChannelModes[mode]
130 if !ok {
131 continue
132 }
133 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
134 if plusMinus == '+' {
135 var argument string
136 // some sentitive arguments (such as channel keys) can be omitted for privacy
137 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
138 if nextArgument < len(arguments) {
139 argument = arguments[nextArgument]
140 }
141 if ch.modes != nil {
142 ch.modes[mode] = argument
143 }
144 } else {
145 delete(ch.modes, mode)
146 }
147 nextArgument++
148 } else if mt == modeTypeC || mt == modeTypeD {
149 if plusMinus == '+' {
150 if ch.modes != nil {
151 ch.modes[mode] = ""
152 }
153 } else {
154 delete(ch.modes, mode)
155 }
156 }
157 }
158 return needMarshaling, nil
159}
160
161func (cm channelModes) Format() (modeString string, parameters []string) {
162 var modesWithValues strings.Builder
163 var modesWithoutValues strings.Builder
164 parameters = make([]string, 0, 16)
165 for mode, value := range cm {
166 if value != "" {
167 modesWithValues.WriteString(string(mode))
168 parameters = append(parameters, value)
169 } else {
170 modesWithoutValues.WriteString(string(mode))
171 }
172 }
173 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
174 return
175}
176
177const stdChannelTypes = "#&+!"
178
179type channelStatus byte
180
181const (
182 channelPublic channelStatus = '='
183 channelSecret channelStatus = '@'
184 channelPrivate channelStatus = '*'
185)
186
187func parseChannelStatus(s string) (channelStatus, error) {
188 if len(s) > 1 {
189 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
190 }
191 switch cs := channelStatus(s[0]); cs {
192 case channelPublic, channelSecret, channelPrivate:
193 return cs, nil
194 default:
195 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
196 }
197}
198
199type membership struct {
200 Mode byte
201 Prefix byte
202}
203
204var stdMemberships = []membership{
205 {'q', '~'}, // founder
206 {'a', '&'}, // protected
207 {'o', '@'}, // operator
208 {'h', '%'}, // halfop
209 {'v', '+'}, // voice
210}
211
212// memberships always sorted by descending membership rank
213type memberships []membership
214
215func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
216 l := *m
217 i := 0
218 for _, availableMembership := range availableMemberships {
219 if i >= len(l) {
220 break
221 }
222 if l[i] == availableMembership {
223 if availableMembership == newMembership {
224 // we already have this membership
225 return
226 }
227 i++
228 continue
229 }
230 if availableMembership == newMembership {
231 break
232 }
233 }
234 // insert newMembership at i
235 l = append(l, membership{})
236 copy(l[i+1:], l[i:])
237 l[i] = newMembership
238 *m = l
239}
240
241func (m *memberships) Remove(oldMembership membership) {
242 l := *m
243 for i, currentMembership := range l {
244 if currentMembership == oldMembership {
245 *m = append(l[:i], l[i+1:]...)
246 return
247 }
248 }
249}
250
251func (m memberships) Format(dc *downstreamConn) string {
252 if !dc.caps["multi-prefix"] {
253 if len(m) == 0 {
254 return ""
255 }
256 return string(m[0].Prefix)
257 }
258 prefixes := make([]byte, len(m))
259 for i, membership := range m {
260 prefixes[i] = membership.Prefix
261 }
262 return string(prefixes)
263}
264
265func parseMessageParams(msg *irc.Message, out ...*string) error {
266 if len(msg.Params) < len(out) {
267 return newNeedMoreParamsError(msg.Command)
268 }
269 for i := range out {
270 if out[i] != nil {
271 *out[i] = msg.Params[i]
272 }
273 }
274 return nil
275}
276
277type batch struct {
278 Type string
279 Params []string
280 Outer *batch // if not-nil, this batch is nested in Outer
281 Label string
282}
283
284// The server-time layout, as defined in the IRCv3 spec.
285const serverTimeLayout = "2006-01-02T15:04:05.000Z"
Note: See TracBrowser for help on using the repository browser.