-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.go
More file actions
257 lines (224 loc) · 7.03 KB
/
Copy pathsync.go
File metadata and controls
257 lines (224 loc) · 7.03 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
package strut
import (
"context"
"encoding/json"
"fmt"
"slices"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/snowflake/v2"
)
// SyncOptions selects what to register and where.
type SyncOptions struct {
// Guild registers to one server, which takes effect immediately. Zero
// registers globally, which Discord propagates over about an hour.
Guild snowflake.ID
// Only keeps the commands it accepts, so a server can be given a
// different set from the rest. Nil keeps everything.
Only func(Meta) bool
// IfChanged reads back what Discord already holds and writes only when it
// differs. Registration is heavily rate limited, so this makes syncing on
// every start up cheap.
IfChanged bool
}
// Sync registers commands with Discord, reporting whether it wrote anything.
func (f *Framework[D]) Sync(ctx context.Context, o SyncOptions, opts ...rest.RequestOpt) (bool, error) {
if err := f.Validate(); err != nil {
return false, err
}
if f.opts.InitOwners {
if err := f.InitOwners(ctx, opts...); err != nil {
return false, err
}
}
want := f.SchemaWhere(o.Only)
if o.IfChanged {
same, err := f.matches(want, o.Guild, opts...)
if err != nil {
return false, err
}
if same {
return false, nil
}
}
var err error
if o.Guild == 0 {
_, err = f.client.Rest.SetGlobalCommands(f.client.ApplicationID, want, opts...)
} else {
_, err = f.client.Rest.SetGuildCommands(f.client.ApplicationID, o.Guild, want, opts...)
}
return err == nil, err
}
// SyncGlobal registers every command globally.
func (f *Framework[D]) SyncGlobal(ctx context.Context, opts ...rest.RequestOpt) error {
_, err := f.Sync(ctx, SyncOptions{}, opts...)
return err
}
// SyncGuild registers every command in one server, which takes effect
// immediately rather than after Discord's global propagation delay.
func (f *Framework[D]) SyncGuild(ctx context.Context, guildID snowflake.ID, opts ...rest.RequestOpt) error {
_, err := f.Sync(ctx, SyncOptions{Guild: guildID}, opts...)
return err
}
// SchemaWhere returns the payloads for the commands keep accepts. A nil keep
// returns everything.
func (f *Framework[D]) SchemaWhere(keep func(Meta) bool) []discord.ApplicationCommandCreate {
var out []discord.ApplicationCommandCreate
for _, c := range f.cmds {
if keep != nil && !keep(c.meta) {
continue
}
out = append(out, c.schema...)
}
return out
}
// Schema returns the payloads for every registered command.
func (f *Framework[D]) Schema() []discord.ApplicationCommandCreate {
return f.SchemaWhere(nil)
}
// matches reports whether Discord already holds exactly these commands.
func (f *Framework[D]) matches(want []discord.ApplicationCommandCreate, guild snowflake.ID, opts ...rest.RequestOpt) (bool, error) {
var (
have []discord.ApplicationCommand
err error
)
if guild == 0 {
have, err = f.client.Rest.GetGlobalCommands(f.client.ApplicationID, true, opts...)
} else {
have, err = f.client.Rest.GetGuildCommands(f.client.ApplicationID, guild, true, opts...)
}
if err != nil {
return false, fmt.Errorf("strut: reading registered commands: %w", err)
}
if len(have) != len(want) {
return false, nil
}
mine := make([]string, 0, len(want))
for _, c := range want {
s, err := signatureOfCreate(c)
if err != nil {
return false, err
}
mine = append(mine, s)
}
theirs := make([]string, 0, len(have))
for _, c := range have {
s, err := signatureOf(c)
if err != nil {
return false, err
}
theirs = append(theirs, s)
}
slices.Sort(mine)
slices.Sort(theirs)
return slices.Equal(mine, theirs), nil
}
// signature is the part of a command Discord stores and strut controls.
// Everything Discord adds of its own, such as ids and versions, is left out
// so the two sides can be compared.
type signature struct {
Type int `json:"type"`
Name string `json:"name"`
NameL10 map[string]string `json:"name_l10n,omitempty"`
Desc string `json:"desc,omitempty"`
DescL10 map[string]string `json:"desc_l10n,omitempty"`
Options json.RawMessage `json:"options,omitempty"`
Perms string `json:"perms,omitempty"`
NSFW bool `json:"nsfw,omitempty"`
Types []int `json:"types,omitempty"`
Ctxs []int `json:"ctxs,omitempty"`
}
func (s signature) String() (string, error) {
b, err := json.Marshal(s)
return string(b), err
}
// signatureOfCreate reads a signature off a payload strut generated.
func signatureOfCreate(c discord.ApplicationCommandCreate) (string, error) {
s := signature{Type: int(c.Type()), Name: c.CommandName()}
switch v := c.(type) {
case discord.SlashCommandCreate:
s.Desc = v.Description
s.NameL10 = locales(v.NameLocalizations)
s.DescL10 = locales(v.DescriptionLocalizations)
s.Perms = permString(v.DefaultMemberPermissions.Value)
s.NSFW = v.NSFW != nil && *v.NSFW
s.Types, s.Ctxs = ints(v.IntegrationTypes), contexts(v.Contexts)
opts, err := marshalOptions(v.Options)
if err != nil {
return "", err
}
s.Options = opts
case discord.UserCommandCreate:
s.NameL10 = locales(v.NameLocalizations)
s.Perms = permString(v.DefaultMemberPermissions.Value)
s.NSFW = v.NSFW != nil && *v.NSFW
s.Types, s.Ctxs = ints(v.IntegrationTypes), contexts(v.Contexts)
case discord.MessageCommandCreate:
s.NameL10 = locales(v.NameLocalizations)
s.Perms = permString(v.DefaultMemberPermissions.Value)
s.NSFW = v.NSFW != nil && *v.NSFW
s.Types, s.Ctxs = ints(v.IntegrationTypes), contexts(v.Contexts)
}
return s.String()
}
// signatureOf reads a signature off a command Discord returned.
func signatureOf(c discord.ApplicationCommand) (string, error) {
s := signature{Type: int(c.Type()), Name: c.Name()}
s.NameL10 = locales(c.NameLocalizations())
s.NSFW = c.NSFW()
s.Types, s.Ctxs = ints(c.IntegrationTypes()), contexts(c.Contexts())
if p := c.DefaultMemberPermissions(); p != discord.PermissionsNone {
s.Perms = p.String()
}
if v, ok := c.(discord.SlashCommand); ok {
s.Desc = v.Description
s.DescL10 = locales(v.DescriptionLocalizations)
opts, err := marshalOptions(v.Options)
if err != nil {
return "", err
}
s.Options = opts
}
return s.String()
}
// marshalOptions renders an option list, which both sides carry in the same
// shape.
func marshalOptions(opts []discord.ApplicationCommandOption) (json.RawMessage, error) {
if len(opts) == 0 {
return nil, nil
}
return json.Marshal(opts)
}
func locales(m map[discord.Locale]string) map[string]string {
if len(m) == 0 {
return nil
}
out := make(map[string]string, len(m))
for k, v := range m {
out[string(k)] = v
}
return out
}
// perms renders permissions, distinguishing unset from an explicit zero.
func permString(p *discord.Permissions) string {
if p == nil {
return ""
}
return p.String()
}
func ints(v []discord.ApplicationIntegrationType) []int {
out := make([]int, len(v))
for i, t := range v {
out[i] = int(t)
}
slices.Sort(out)
return out
}
func contexts(v []discord.InteractionContextType) []int {
out := make([]int, len(v))
for i, t := range v {
out[i] = int(t)
}
slices.Sort(out)
return out
}