From 0d377f09ea9c566492e20a1da5be6427c61e6883 Mon Sep 17 00:00:00 2001 From: Liz Date: Wed, 9 Aug 2023 23:45:15 +0100 Subject: [PATCH 1/5] initial user option impl --- .../Option/UserOption.swift | 36 +++++++++++++++++++ Sources/DiscordKitBot/BotMessage.swift | 21 +++++++---- .../Objects/Data/Interaction.swift | 8 ++++- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift diff --git a/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift b/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift new file mode 100644 index 000000000..e8a38e260 --- /dev/null +++ b/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift @@ -0,0 +1,36 @@ +// +// File.swift +// +// +// Created by Elizabeth (lizclipse) on 09/08/2023. +// + +import Foundation +import DiscordKitCore + +public struct UserOption: CommandOption { + public init(_ name: String, description: String, `required`: Bool? = nil, choices: [AppCommandOptionChoice]? = nil, minLength: Int? = nil, maxLength: Int? = nil, autocomplete: Bool? = nil) { + type = .user + + self.required = `required` + self.choices = choices + self.name = name + self.description = description + self.autocomplete = autocomplete + } + + public var type: CommandOptionType + + public var required: Bool? + + /// Choices for the user to pick from + /// + /// > Important: There can be a max of 25 choices. + public let choices: [AppCommandOptionChoice]? + + public let name: String + public let description: String + + /// If autocomplete interactions are enabled for this option + public let autocomplete: Bool? +} diff --git a/Sources/DiscordKitBot/BotMessage.swift b/Sources/DiscordKitBot/BotMessage.swift index 847aa1158..21705adeb 100644 --- a/Sources/DiscordKitBot/BotMessage.swift +++ b/Sources/DiscordKitBot/BotMessage.swift @@ -14,17 +14,26 @@ import DiscordKitCore /// > Internally, `Message`s are converted to and from this type /// > for easier use public struct BotMessage { - public let content: String - public let channelID: Snowflake // This will be changed very soon - public let id: Snowflake // This too +// public let content: String +// public let channelID: Snowflake // This will be changed very soon +// public let id: Snowflake // This too +// public let author: User + + public var content: String { get { return inner.content } } + public var channelID: Snowflake { get { return inner.channel_id } } + public var id: Snowflake { get { return inner.id } } + + public let inner: Message // The REST handler associated with this message, used for message actions fileprivate weak var rest: DiscordREST? internal init(from message: Message, rest: DiscordREST) { - content = message.content - channelID = message.channel_id - id = message.id + self.inner = message +// content = message.content +// channelID = message.channel_id +// id = message.id +// author = message.author self.rest = rest } diff --git a/Sources/DiscordKitCore/Objects/Data/Interaction.swift b/Sources/DiscordKitCore/Objects/Data/Interaction.swift index 415398048..ad610b34b 100644 --- a/Sources/DiscordKitCore/Objects/Data/Interaction.swift +++ b/Sources/DiscordKitCore/Objects/Data/Interaction.swift @@ -72,6 +72,7 @@ public struct Interaction: Decodable { case integer(Int) case double(Double) case boolean(Bool) // Discord docs are disappointing + case user(Snowflake) public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() @@ -80,6 +81,7 @@ public struct Interaction: Decodable { case .integer(let val): try container.encode(val) case .double(let val): try container.encode(val) case .boolean(let val): try container.encode(val) + case .user(let val): try container.encode(val) } } @@ -87,7 +89,10 @@ public struct Interaction: Decodable { /// /// - Returns: The string value of a certain option if it is present and is of type `String`, otherwise `nil` public func value() -> String? { - guard case let .string(val) = self else { return nil } + guard case let .string(val) = self else { + guard case let .user(val) = self else { return nil } + return val + } return val } /// Get the wrapped `Int` value @@ -145,6 +150,7 @@ public struct Interaction: Decodable { case .number: value = .double(try container.decode(Double.self, forKey: .value)) case .boolean: value = .boolean(try container.decode(Bool.self, forKey: .value)) case .string: value = .string(try container.decode(String.self, forKey: .value)) + case .user: value = .user(try container.decode(Snowflake.self, forKey: .value)) default: value = nil } } From 46dbc4b3683276da2c992936a5a00da3f9122eb4 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Tue, 12 Nov 2024 11:12:11 +0000 Subject: [PATCH 2/5] add remaining BotMessage fields & ext method --- Sources/DiscordKitBot/BotMessage.swift | 64 ++++++++++++++++++-------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/Sources/DiscordKitBot/BotMessage.swift b/Sources/DiscordKitBot/BotMessage.swift index 21705adeb..59e83e997 100644 --- a/Sources/DiscordKitBot/BotMessage.swift +++ b/Sources/DiscordKitBot/BotMessage.swift @@ -1,12 +1,12 @@ // // BotMessage.swift -// +// // // Created by Vincent Kwok on 22/11/22. // -import Foundation import DiscordKitCore +import Foundation /// A Discord message, with convenience methods /// @@ -14,15 +14,41 @@ import DiscordKitCore /// > Internally, `Message`s are converted to and from this type /// > for easier use public struct BotMessage { -// public let content: String -// public let channelID: Snowflake // This will be changed very soon -// public let id: Snowflake // This too -// public let author: User - - public var content: String { get { return inner.content } } - public var channelID: Snowflake { get { return inner.channel_id } } - public var id: Snowflake { get { return inner.id } } - + public var id: Snowflake { return inner.id } + public var channelID: Snowflake { return inner.channel_id } + public var guildID: Snowflake? { return inner.guild_id } + public var author: User { return inner.author } + public var member: Member? { return inner.member } + public var timestamp: Date { return inner.timestamp } + public var editedTimestamp: Date? { return inner.edited_timestamp } + public var tts: Bool { return inner.tts } + public var mentionEveryone: Bool { return inner.mention_everyone } + public var mentions: [User] { return inner.mentions } + public var mentionRoles: [Snowflake] { return inner.mention_roles } + public var mentionChannels: [ChannelMention]? { return inner.mention_channels } + public var attachments: [Attachment] { return inner.attachments } + public var embeds: [Embed] { return inner.embeds } + public var reactions: [Reaction]? { return inner.reactions } + public var nonce: Nonce? { return inner.nonce } + public var pinned: Bool { return inner.pinned } + public var webhookId: Snowflake? { return inner.webhook_id } + public var type: MessageType { return inner.type } + public var activity: MessageActivity? { return inner.activity } + public var application: Application? { return inner.application } + public var applicationId: Snowflake? { return inner.application_id } + public var messageReference: MessageReference? { return inner.message_reference } + public var flags: Int? { return inner.flags } + public var referencedMessage: BotMessage? { + guard let ref = inner.referenced_message else { return nil } + return Self(from: ref, rest: self.rest!) + } + public var interaction: MessageInteraction? { return inner.interaction } + public var thread: Channel? { return inner.thread } + public var components: [MessageComponent]? { return inner.components } + public var stickerItems: [StickerItem]? { return inner.sticker_items } + public var call: CallMessageComponent? { return inner.call } + public var content: String { return inner.content } + public let inner: Message // The REST handler associated with this message, used for message actions @@ -30,20 +56,20 @@ public struct BotMessage { internal init(from message: Message, rest: DiscordREST) { self.inner = message -// content = message.content -// channelID = message.channel_id -// id = message.id -// author = message.author - self.rest = rest } } -public extension BotMessage { - func reply(_ content: String) async throws -> Message { +extension BotMessage { + public func reply(_ content: String) async throws -> Message { return try await rest!.createChannelMsg( - message: .init(content: content, message_reference: .init(message_id: id), components: []), + message: .init( + content: content, message_reference: .init(message_id: id), components: []), id: channelID ) } + + public func mentions(_ userID: Snowflake) -> Bool { + return mentions.first(identifiedBy: userID) != nil + } } From 78d6747cadf5c4d1496e89399bdafe412a4885c8 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Tue, 11 Feb 2025 11:05:15 +0000 Subject: [PATCH 3/5] add channel option --- .../Option/ChannelOption.swift | 30 +++++++++++++++++++ .../Option/UserOption.swift | 2 +- .../Objects/Data/Interaction.swift | 13 ++++---- 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 Sources/DiscordKitBot/ApplicationCommand/Option/ChannelOption.swift diff --git a/Sources/DiscordKitBot/ApplicationCommand/Option/ChannelOption.swift b/Sources/DiscordKitBot/ApplicationCommand/Option/ChannelOption.swift new file mode 100644 index 000000000..012978336 --- /dev/null +++ b/Sources/DiscordKitBot/ApplicationCommand/Option/ChannelOption.swift @@ -0,0 +1,30 @@ +// +// ChannelOption.swift +// +// +// Created by Elizabeth on 02/10/2025. +// + +import Foundation +import DiscordKitCore + +/// An option for an application command that accepts a server channel +public struct ChannelOption: CommandOption { + public init(_ name: String, description: String, `required`: Bool? = nil, channel_types: [ChannelType]? = nil) { + type = .channel + + self.required = `required` + self.name = name + self.description = description + self.channel_types = channel_types + } + + public var type: CommandOptionType + + public var required: Bool? + + public let name: String + public let description: String + + public let channel_types: [ChannelType]? +} diff --git a/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift b/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift index e8a38e260..65a96ba88 100644 --- a/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift +++ b/Sources/DiscordKitBot/ApplicationCommand/Option/UserOption.swift @@ -1,5 +1,5 @@ // -// File.swift +// UserOption.swift // // // Created by Elizabeth (lizclipse) on 09/08/2023. diff --git a/Sources/DiscordKitCore/Objects/Data/Interaction.swift b/Sources/DiscordKitCore/Objects/Data/Interaction.swift index ad610b34b..ecc999a39 100644 --- a/Sources/DiscordKitCore/Objects/Data/Interaction.swift +++ b/Sources/DiscordKitCore/Objects/Data/Interaction.swift @@ -73,6 +73,7 @@ public struct Interaction: Decodable { case double(Double) case boolean(Bool) // Discord docs are disappointing case user(Snowflake) + case channel(String) public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() @@ -82,6 +83,7 @@ public struct Interaction: Decodable { case .double(let val): try container.encode(val) case .boolean(let val): try container.encode(val) case .user(let val): try container.encode(val) + case .channel(let val): try container.encode(val) } } @@ -89,11 +91,11 @@ public struct Interaction: Decodable { /// /// - Returns: The string value of a certain option if it is present and is of type `String`, otherwise `nil` public func value() -> String? { - guard case let .string(val) = self else { - guard case let .user(val) = self else { return nil } - return val - } - return val + if case let .string(val) = self { return val } + if case let .user(val) = self { return val } + if case let .channel(val) = self { return val } + + return nil } /// Get the wrapped `Int` value /// @@ -151,6 +153,7 @@ public struct Interaction: Decodable { case .boolean: value = .boolean(try container.decode(Bool.self, forKey: .value)) case .string: value = .string(try container.decode(String.self, forKey: .value)) case .user: value = .user(try container.decode(Snowflake.self, forKey: .value)) + case .channel: value = .channel(try container.decode(String.self, forKey: .value)) default: value = nil } } From fc1dbb6ccf7d1cf07e274d2d890709ace4337080 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Thu, 20 Feb 2025 13:19:13 +0000 Subject: [PATCH 4/5] added subcommand groups --- .../ApplicationCommand/CommandData.swift | 15 ++++ .../Option/SubCommandGroup.swift | 69 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 Sources/DiscordKitBot/ApplicationCommand/Option/SubCommandGroup.swift diff --git a/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift b/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift index 360766262..84698a763 100644 --- a/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift +++ b/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift @@ -87,6 +87,21 @@ public extension CommandData { typealias OptionData = Interaction.Data.AppCommandData.OptionData } +public extension CommandData { + func subGroup(name: String) -> CommandData? { + guard let option = optionValues[name], option.type == .subCommandGroup else { return nil } + guard let options = option.options else { return nil } + guard let rest = self.rest else { return nil } + return CommandData( + optionValues: options, + rest: rest, + applicationID: self.applicationID, + interactionID: self.interactionID, + token: self.token + ) + } +} + // MARK: - Callback APIs public extension CommandData { /// Wrapper function to send an interaction response with the current interaction's ID and token diff --git a/Sources/DiscordKitBot/ApplicationCommand/Option/SubCommandGroup.swift b/Sources/DiscordKitBot/ApplicationCommand/Option/SubCommandGroup.swift new file mode 100644 index 000000000..e88daa264 --- /dev/null +++ b/Sources/DiscordKitBot/ApplicationCommand/Option/SubCommandGroup.swift @@ -0,0 +1,69 @@ +// +// SubCommandGroup.swift +// +// +// Created by Elizabeth on 11/02/2025. +// + +import DiscordKitCore +import Foundation + +public struct SubCommandGroup: CommandOption { + /// Create a sub-command, optionally with an array of options + public init(_ name: String, description: String, options: [SubCommand]? = nil) { + type = .subCommandGroup + + self.name = name + self.description = description + self.options = options + } + + /// Create a sub-command with options built by an ``OptionBuilder`` + public init( + _ name: String, description: String, @SubCommandOptionBuilder options: () -> [SubCommand] + ) { + self.init(name, description: description, options: options()) + } + + public let type: CommandOptionType + + public let name: String + + public let description: String + + public var required: Bool? + + /// If this command is a subcommand or subcommand group type, these nested options will be its parameters + public let options: [SubCommand]? + + enum CodingKeys: CodingKey { + case type + case name + case description + case required + case options + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container( + keyedBy: SubCommand.CodingKeys.self) + + try container.encode(self.type, forKey: SubCommand.CodingKeys.type) + try container.encode(self.name, forKey: SubCommand.CodingKeys.name) + try container.encode(self.description, forKey: SubCommand.CodingKeys.description) + try container.encodeIfPresent(self.required, forKey: SubCommand.CodingKeys.required) + if let options = options { + var optContainer = container.nestedUnkeyedContainer(forKey: .options) + for option in options { + try optContainer.encode(option) + } + } + } +} + +@resultBuilder +public struct SubCommandOptionBuilder { + public static func buildBlock(_ components: SubCommand...) -> [SubCommand] { + components + } +} From c5dcf65d4d4991e40314e42c0c7b4092542bec68 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Fri, 21 Feb 2025 14:47:56 +0000 Subject: [PATCH 5/5] added guild ID & resovled data to CommandData --- .../ApplicationCommand/CommandData.swift | 19 +++++++++++++++++-- Sources/DiscordKitBot/Client.swift | 11 ++++++++--- .../Objects/Data/Interaction.swift | 6 ++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift b/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift index 84698a763..10c231550 100644 --- a/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift +++ b/Sources/DiscordKitBot/ApplicationCommand/CommandData.swift @@ -12,14 +12,21 @@ import DiscordKitCore public class CommandData { internal init( optionValues: [OptionData], - rest: DiscordREST, applicationID: String, interactionID: Snowflake, token: String + rest: DiscordREST, + applicationID: String, + guildID: Snowflake?, + interactionID: Snowflake, + token: String, + resolved: ResolvedData? ) { self.rest = rest self.token = token + self.guildID = guildID self.interactionID = interactionID self.applicationID = applicationID self.optionValues = Self.unwrapOptionDatas(optionValues) + self.resolved = resolved } /// A private reference to the active rest handler for handling actions @@ -40,9 +47,14 @@ public class CommandData { // MARK: Parameters for executing callbacks /// The token to use when carrying out actions with this interaction let token: String + + public let guildID: Snowflake? + /// The ID of this interaction public let interactionID: Snowflake + public let resolved: ResolvedData? + fileprivate static func unwrapOptionDatas(_ options: [OptionData]) -> [String: OptionData] { var optValues: [String: OptionData] = [:] for optionValue in options { @@ -85,6 +97,7 @@ public extension CommandData { /// The wrapped value of an option typealias OptionData = Interaction.Data.AppCommandData.OptionData + typealias ResolvedData = Interaction.Data.AppCommandData.ResolvedData } public extension CommandData { @@ -96,8 +109,10 @@ public extension CommandData { optionValues: options, rest: rest, applicationID: self.applicationID, + guildID: self.guildID, interactionID: self.interactionID, - token: self.token + token: self.token, + resolved: self.resolved ) } } diff --git a/Sources/DiscordKitBot/Client.swift b/Sources/DiscordKitBot/Client.swift index d95a411dc..9e36c979f 100644 --- a/Sources/DiscordKitBot/Client.swift +++ b/Sources/DiscordKitBot/Client.swift @@ -108,13 +108,18 @@ extension Client { public var isReady: Bool { gateway?.sessionOpen == true } /// Invoke the handler associated with the respective commands - private func invokeCommandHandler(_ commandData: Interaction.Data.AppCommandData, id: Snowflake, token: String) { + private func invokeCommandHandler(_ commandData: Interaction.Data.AppCommandData, id: Snowflake, token: String, guildID: Snowflake?) { if let handler = appCommandHandlers[commandData.id] { Self.logger.trace("Invoking application handler", metadata: ["command.name": "\(commandData.name)"]) Task { await handler(.init( optionValues: commandData.options ?? [], - rest: rest, applicationID: applicationID!, interactionID: id, token: token + rest: rest, + applicationID: applicationID!, + guildID: guildID, + interactionID: id, + token: token, + resolved: commandData.resolved )) } } @@ -143,7 +148,7 @@ extension Client { // Handle interactions based on type switch interaction.data { case .applicationCommand(let commandData): - invokeCommandHandler(commandData, id: interaction.id, token: interaction.token) + invokeCommandHandler(commandData, id: interaction.id, token: interaction.token, guildID: interaction.guildID) case .messageComponent(let componentData): print("Component interaction: \(componentData.custom_id)") default: break diff --git a/Sources/DiscordKitCore/Objects/Data/Interaction.swift b/Sources/DiscordKitCore/Objects/Data/Interaction.swift index ecc999a39..9e0cd4705 100644 --- a/Sources/DiscordKitCore/Objects/Data/Interaction.swift +++ b/Sources/DiscordKitCore/Objects/Data/Interaction.swift @@ -61,6 +61,8 @@ public struct Interaction: Decodable { public let name: String /// Type of command public let type: Int + /// Resolved references for things like user and channels + public let resolved: ResolvedData? /// Options of command (present if the command has options) public let options: [OptionData]? @@ -158,6 +160,10 @@ public struct Interaction: Decodable { } } } + + public struct ResolvedData: Codable { + public let channels: [Snowflake: Channel]? + } } /// The data payload for message component interactions