source: code/trunk/irc.go@ 292

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

Add support for multiple user channel memberships

User channel memberships are actually a set of memberships, not a single
value. This introduces memberships, a type representing a set of
memberships, stored as an array of memberships ordered by descending
rank.

This also adds multi-prefix to the permanent downstream and upstream
capabilities, so that we try to get all possible channel memberships.

File size: 5.6 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
87func (cm channelModes) Apply(modeTypes map[byte]channelModeType, modeStr string, arguments ...string) error {
88 nextArgument := 0
89 var plusMinus byte
90 for i := 0; i < len(modeStr); i++ {
91 mode := modeStr[i]
92 if mode == '+' || mode == '-' {
93 plusMinus = mode
94 continue
95 }
96 if plusMinus != '+' && plusMinus != '-' {
97 return fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
98 }
99
100 mt, ok := modeTypes[mode]
101 if !ok {
102 continue
103 }
104 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
105 if plusMinus == '+' {
106 var argument string
107 // some sentitive arguments (such as channel keys) can be omitted for privacy
108 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
109 if nextArgument < len(arguments) {
110 argument = arguments[nextArgument]
111 }
112 cm[mode] = argument
113 } else {
114 delete(cm, mode)
115 }
116 nextArgument++
117 } else if mt == modeTypeC || mt == modeTypeD {
118 if plusMinus == '+' {
119 cm[mode] = ""
120 } else {
121 delete(cm, mode)
122 }
123 }
124 }
125 return nil
126}
127
128func (cm channelModes) Format() (modeString string, parameters []string) {
129 var modesWithValues strings.Builder
130 var modesWithoutValues strings.Builder
131 parameters = make([]string, 0, 16)
132 for mode, value := range cm {
133 if value != "" {
134 modesWithValues.WriteString(string(mode))
135 parameters = append(parameters, value)
136 } else {
137 modesWithoutValues.WriteString(string(mode))
138 }
139 }
140 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
141 return
142}
143
144const stdChannelTypes = "#&+!"
145
146type channelStatus byte
147
148const (
149 channelPublic channelStatus = '='
150 channelSecret channelStatus = '@'
151 channelPrivate channelStatus = '*'
152)
153
154func parseChannelStatus(s string) (channelStatus, error) {
155 if len(s) > 1 {
156 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
157 }
158 switch cs := channelStatus(s[0]); cs {
159 case channelPublic, channelSecret, channelPrivate:
160 return cs, nil
161 default:
162 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
163 }
164}
165
166type membership struct {
167 Mode byte
168 Prefix byte
169}
170
171var stdMemberships = []membership{
172 {'q', '~'}, // founder
173 {'a', '&'}, // protected
174 {'o', '@'}, // operator
175 {'h', '%'}, // halfop
176 {'v', '+'}, // voice
177}
178
179// memberships always sorted by descending membership rank
180type memberships []membership
181
182func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
183 l := *m
184 i := 0
185 for _, availableMembership := range availableMemberships {
186 if i >= len(l) {
187 break
188 }
189 if l[i] == availableMembership {
190 if availableMembership == newMembership {
191 // we already have this membership
192 return
193 }
194 i++
195 continue
196 }
197 if availableMembership == newMembership {
198 break
199 }
200 }
201 // insert newMembership at i
202 l = append(l, membership{})
203 copy(l[i+1:], l[i:])
204 l[i] = newMembership
205 *m = l
206}
207
208func (m *memberships) Remove(oldMembership membership) {
209 l := *m
210 for i, currentMembership := range l {
211 if currentMembership == oldMembership {
212 *m = append(l[:i], l[i+1:]...)
213 return
214 }
215 }
216}
217
218func (m memberships) Format(dc *downstreamConn) string {
219 if !dc.caps["multi-prefix"] {
220 if len(m) == 0 {
221 return ""
222 }
223 return string(m[0].Prefix)
224 }
225 prefixes := make([]byte, len(m))
226 for i, membership := range m {
227 prefixes[i] = membership.Prefix
228 }
229 return string(prefixes)
230}
231
232func parseMessageParams(msg *irc.Message, out ...*string) error {
233 if len(msg.Params) < len(out) {
234 return newNeedMoreParamsError(msg.Command)
235 }
236 for i := range out {
237 if out[i] != nil {
238 *out[i] = msg.Params[i]
239 }
240 }
241 return nil
242}
243
244type batch struct {
245 Type string
246 Params []string
247 Outer *batch // if not-nil, this batch is nested in Outer
248 Label string
249}
250
251// The server-time layout, as defined in the IRCv3 spec.
252const serverTimeLayout = "2006-01-02T15:04:05.000Z"
Note: See TracBrowser for help on using the repository browser.