-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecode.go
More file actions
323 lines (289 loc) · 9.09 KB
/
Copy pathdecode.go
File metadata and controls
323 lines (289 loc) · 9.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package strut
import (
"fmt"
"reflect"
"time"
"unicode/utf8"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/snowflake/v2"
)
// ArgumentError is a single option that could not be supplied. It is wrapped
// in an Error with kind ErrArgumentParse before reaching a handler.
type ArgumentError struct {
Field string
Input string
Err error
}
func (e *ArgumentError) Error() string {
if e.Err != nil {
return fmt.Sprintf("option %q: %v", e.Field, e.Err)
}
return fmt.Sprintf("option %q is required", e.Field)
}
func (e *ArgumentError) Unwrap() error { return e.Err }
// setter fills one struct field from interaction data.
type setter struct {
index []int
fn func(dst reflect.Value, d discord.SlashCommandInteractionData) error
}
// decoder turns interaction data into a command struct. One is built per
// command when it is added, so the invoke path does no tag or type analysis.
type decoder struct {
typ reflect.Type // the struct type, never a pointer
setters []setter
// types records each option's wire type, so a test harness can build
// interaction data without restating it.
types map[string]discord.ApplicationCommandOptionType
// text is set only for commands exposed on the Prefix surface.
text []textSetter
userTarget []int // field path, or nil
messageTarget []int
}
// newDecoder compiles a spec into setters.
func newDecoder(spec *structSpec) (*decoder, error) {
d := &decoder{
typ: spec.Type,
types: make(map[string]discord.ApplicationCommandOptionType, len(spec.Fields)),
}
if spec.UserTarget != nil {
d.userTarget = spec.UserTarget.Index
}
if spec.MessageTarget != nil {
d.messageTarget = spec.MessageTarget.Index
}
for _, f := range spec.Fields {
if f.Variadic {
continue
}
fn, err := fieldSetter(f)
if err != nil {
return nil, err
}
k := f.Kind
if k == kindCustom {
k = customKind(f.Type)
}
d.types[f.Tag.Name] = k.optionType()
d.setters = append(d.setters, setter{index: f.Index, fn: fn})
}
return d, nil
}
// decode copies proto and fills the copy. Copying, not allocating fresh, is
// what preserves unexported dependencies set on the registered command.
func (dec *decoder) decode(proto reflect.Value, data discord.SlashCommandInteractionData) (reflect.Value, error) {
out := reflect.New(dec.typ)
out.Elem().Set(proto)
for _, s := range dec.setters {
if err := s.fn(out.Elem().FieldByIndex(s.index), data); err != nil {
return reflect.Value{}, err
}
}
return out, nil
}
// fillUserTarget sets the UserTarget field, if the command declares one.
func (dec *decoder) fillUserTarget(v reflect.Value, t UserTarget) {
if dec.userTarget != nil {
v.Elem().FieldByIndex(dec.userTarget).Set(reflect.ValueOf(t))
}
}
// fillMessageTarget sets the MessageTarget field, if the command declares one.
func (dec *decoder) fillMessageTarget(v reflect.Value, t MessageTarget) {
if dec.messageTarget != nil {
v.Elem().FieldByIndex(dec.messageTarget).Set(reflect.ValueOf(t))
}
}
// validator rejects a decoded value before it reaches a command.
type validator func(reflect.Value) error
// store writes into a field, wrapping when it is an Option. The conversion is
// for named types: type Severity string does not match disgo's string.
func store(dst reflect.Value, rv reflect.Value, target reflect.Type, optional bool, check validator) error {
if rv.Type() != target {
if !rv.CanConvert(target) {
return fmt.Errorf("cannot convert %s to %s", rv.Type(), target)
}
rv = rv.Convert(target)
}
if check != nil {
if err := check(rv); err != nil {
return err
}
}
if optional {
setOption(dst, rv)
return nil
}
dst.Set(rv)
return nil
}
// fieldValidator re-checks the constraints already declared to Discord.
// Discord enforces them in its client, but the interaction payload is user
// controlled and a crafted request can carry anything.
func fieldValidator(f fieldSpec) (validator, error) {
var checks []validator
if f.ChoiceElem != nil {
raw, err := rawChoices(f)
if err != nil {
return nil, err
}
allowed := make(map[any]struct{}, len(raw))
for _, c := range raw {
allowed[c.Value.Interface()] = struct{}{}
}
checks = append(checks, func(v reflect.Value) error {
if _, ok := allowed[v.Interface()]; !ok {
return fmt.Errorf("%v is not an allowed choice", v.Interface())
}
return nil
})
}
lo, hasLo := f.Tag.Min.Load()
hi, hasHi := f.Tag.Max.Load()
_, isLength := f.Kind.bounded()
switch {
case !hasLo && !hasHi:
case isLength:
checks = append(checks, func(v reflect.Value) error {
n := float64(utf8.RuneCountInString(v.String()))
if hasLo && n < lo {
return fmt.Errorf("is %v characters, minimum %v", n, lo)
}
if hasHi && n > hi {
return fmt.Errorf("is %v characters, maximum %v", n, hi)
}
return nil
})
default:
checks = append(checks, func(v reflect.Value) error {
n := numeric(v)
if hasLo && n < lo {
return fmt.Errorf("is %v, minimum %v", n, lo)
}
if hasHi && n > hi {
return fmt.Errorf("is %v, maximum %v", n, hi)
}
return nil
})
}
switch len(checks) {
case 0:
return nil, nil
case 1:
return checks[0], nil
}
return func(v reflect.Value) error {
for _, c := range checks {
if err := c(v); err != nil {
return err
}
}
return nil
}, nil
}
func numeric(v reflect.Value) float64 {
switch v.Kind() {
case reflect.Float32, reflect.Float64:
return v.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint())
}
return float64(v.Int())
}
// bind builds a setter from one of disgo's Opt accessors.
func bind[T any](f fieldSpec, check validator, get func(discord.SlashCommandInteractionData, string) (T, bool)) func(reflect.Value, discord.SlashCommandInteractionData) error {
name, target, optional := f.Tag.Name, f.Type, f.Optional
return func(dst reflect.Value, d discord.SlashCommandInteractionData) error {
v, ok := get(d, name)
if !ok {
if optional {
return nil // absent stays absent
}
return &ArgumentError{Field: name}
}
if err := store(dst, reflect.ValueOf(v), target, optional, check); err != nil {
return &ArgumentError{Field: name, Input: fmt.Sprint(v), Err: err}
}
return nil
}
}
// parsed builds a setter for a type Discord sends as a string but Go wants in
// another form.
func parsed[T any](f fieldSpec, check validator, parse func(string) (T, error)) func(reflect.Value, discord.SlashCommandInteractionData) error {
name, target, optional := f.Tag.Name, f.Type, f.Optional
return func(dst reflect.Value, d discord.SlashCommandInteractionData) error {
s, ok := d.OptString(name)
if !ok {
if optional {
return nil
}
return &ArgumentError{Field: name}
}
v, err := parse(s)
if err != nil {
return &ArgumentError{Field: name, Input: s, Err: err}
}
if err := store(dst, reflect.ValueOf(v), target, optional, check); err != nil {
return &ArgumentError{Field: name, Input: s, Err: err}
}
return nil
}
}
// custom builds a setter for a type implementing Argument.
func custom(f fieldSpec, check validator) func(reflect.Value, discord.SlashCommandInteractionData) error {
name, target, optional := f.Tag.Name, f.Type, f.Optional
return func(dst reflect.Value, d discord.SlashCommandInteractionData) error {
if _, ok := d.Option(name); !ok {
if optional {
return nil
}
return &ArgumentError{Field: name}
}
v := reflect.New(target)
if err := v.Interface().(Argument).FromInteraction(d, name); err != nil {
return &ArgumentError{Field: name, Err: err}
}
if err := store(dst, v.Elem(), target, optional, check); err != nil {
return &ArgumentError{Field: name, Err: err}
}
return nil
}
}
func fieldSetter(f fieldSpec) (func(reflect.Value, discord.SlashCommandInteractionData) error, error) {
check, err := fieldValidator(f)
if err != nil {
return nil, err
}
if f.Kind == kindCustom {
return custom(f, check), nil
}
// Parsed here rather than via Argument, which cannot be implemented on
// types from other packages.
switch f.Type {
case snowflakeType:
return parsed(f, check, snowflake.Parse), nil
case durationType:
return parsed(f, check, time.ParseDuration), nil
}
switch f.Kind {
case kindString:
return bind(f, check, discord.SlashCommandInteractionData.OptString), nil
case kindInt:
return bind(f, check, discord.SlashCommandInteractionData.OptInt), nil
case kindFloat:
return bind(f, check, discord.SlashCommandInteractionData.OptFloat), nil
case kindBool:
return bind(f, check, discord.SlashCommandInteractionData.OptBool), nil
case kindUser:
return bind(f, check, discord.SlashCommandInteractionData.OptUser), nil
case kindMember:
return bind(f, check, discord.SlashCommandInteractionData.OptMember), nil
case kindChannel:
return bind(f, check, discord.SlashCommandInteractionData.OptChannel), nil
case kindRole:
return bind(f, check, discord.SlashCommandInteractionData.OptRole), nil
case kindMentionable:
return bind(f, check, discord.SlashCommandInteractionData.OptMentionable), nil
case kindAttachment:
return bind(f, check, discord.SlashCommandInteractionData.OptAttachment), nil
}
return nil, fmt.Errorf("option %q: cannot decode %s", f.Tag.Name, f.Kind)
}