///|
/// Defaults that Discord may add to or omit from response objects.
fn object_defaults(command_root : Bool) -> Array[(String, Json)] {
let defaults = [
("required", Json::boolean(false)),
("options", Json::array([])),
("choices", Json::array([])),
]
if command_root {
defaults.push(("type", Json::number(1.0)))
defaults.push(("nsfw", Json::boolean(false)))
defaults.push(("dm_permission", Json::boolean(true)))
defaults.push(("integration_types", Json::array([Json::number(0.0)])))
defaults.push(("contexts", Json::null()))
defaults.push(("default_member_permissions", Json::null()))
}
defaults
}
///|
fn object_default(key : String, command_root : Bool) -> Json? {
for entry in object_defaults(command_root) {
let (candidate, value) = entry
if candidate == key {
return Some(value)
}
}
None
}
///|
/// Compare one declared JSON subtree with the corresponding Discord value.
/// Discord may add fields to response objects; only keys emitted by the
/// declaration and known declaration defaults participate in equality.
fn declared_json_matches_at(
declared : Json,
fetched : Json,
command_root : Bool,
) -> Bool {
match (declared, fetched) {
(Object(expected), Object(actual)) => {
for key, value in expected {
match actual.get(key) {
Some(other) =>
if !declared_json_matches_at(value, other, false) {
return false
}
None => {
guard object_default(key, command_root) is Some(fallback) else {
return false
}
if !declared_json_matches_at(value, fallback, false) {
return false
}
}
}
}
for entry in object_defaults(command_root) {
let (key, fallback) = entry
if !expected.contains(key) &&
actual.get(key) is Some(value) &&
!declared_json_matches_at(fallback, value, false) {
return false
}
}
true
}
(Array(expected), Array(actual)) => {
if expected.length() != actual.length() {
return false
}
for i, value in expected {
if !declared_json_matches_at(value, actual[i], false) {
return false
}
}
true
}
_ => declared == fetched
}
}
///|
fn declared_json_matches(declared : Json, fetched : Json) -> Bool {
declared_json_matches_at(declared, fetched, true)
}
///|
fn command_key(typ : @model.ApplicationCommandType, name : String) -> String {
"\{typ.to_int()}:\{name}"
}
///|
/// Whether Discord's registered commands already match the declarations.
///
/// Command `(type, name)` pairs must form the same set. For each command,
/// comparison follows the declaration shape recursively, ignoring server-owned
/// fields such as ids and versions while preserving option and choice order.
pub fn commands_in_sync(
declared : Array[@interaction.CommandSpec],
fetched : Array[@model.ApplicationCommand],
) -> Bool {
if declared.length() != fetched.length() {
return false
}
let by_key : Map[String, @model.ApplicationCommand] = Map([])
for command in fetched {
let typ = command.typ.unwrap_or(ChatInput)
let key = command_key(typ, command.name)
if by_key.contains(key) {
return false
}
by_key[key] = command
}
let declared_keys : Set[String] = Set([])
for spec in declared {
let key = command_key(spec.typ, spec.name)
if declared_keys.contains(key) {
return false
}
declared_keys.add(key)
guard by_key.get(key) is Some(command) else { return false }
if !declared_json_matches(spec.to_json(), command.to_json()) {
return false
}
}
true
}
///|
fn command_payload(specs : Array[@interaction.CommandSpec]) -> Json {
Json::array(specs.map(spec => spec.to_json()))
}
///|
async fn sync_global_commands(
client : @dhttp.Client,
application_id : @model.ApplicationId,
specs : Array[@interaction.CommandSpec],
) -> Unit {
let fetched = client.get_global_commands(
application_id,
with_localizations=true,
)
if !commands_in_sync(specs, fetched) {
client.bulk_overwrite_global_commands(
application_id,
command_payload(specs),
)
|> ignore
}
}
///|
async fn sync_guild_commands(
client : @dhttp.Client,
application_id : @model.ApplicationId,
guild_id : @model.GuildId,
specs : Array[@interaction.CommandSpec],
) -> Unit {
let fetched = client.get_guild_commands(
application_id,
guild_id,
with_localizations=true,
)
if !commands_in_sync(specs, fetched) {
client.bulk_overwrite_guild_commands(
application_id,
guild_id,
command_payload(specs),
)
|> ignore
}
}