source: code/trunk/irc.go@ 315

Last change on this file since 315 was 303, checked in by delthas, 5 years ago

Add support for TAGMSG and client message tags

Previously we dropped all TAGMSG as well as any client message tag sent
from downstream.

This adds support for properly forwarding TAGMSG and client message tags
from downstreams and upstreams.

TAGMSG messages are intentionally not logged, because they are currently
typically used for +typing, which can generate a lot of traffic and is
only useful for a few seconds after it is sent.

File size: 7.0 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
277func copyClientTags(tags irc.Tags) irc.Tags {
278 t := make(irc.Tags, len(tags))
279 for k, v := range tags {
280 if strings.HasPrefix(k, "+") {
281 t[k] = v
282 }
283 }
284 return t
285}
286
287type batch struct {
288 Type string
289 Params []string
290 Outer *batch // if not-nil, this batch is nested in Outer
291 Label string
292}
293
294// The server-time layout, as defined in the IRCv3 spec.
295const serverTimeLayout = "2006-01-02T15:04:05.000Z"
Note: See TracBrowser for help on using the repository browser.