source: code/trunk/irc.go@ 347

Last change on this file since 347 was 346, checked in by hubert, 5 years ago

Send compact channel name lists

This commit resolves sendNames' TODO.

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