# frozen_string_literal: true
RSpec.shared_context("command") do
attr_reader :previous_command
# Override in a spec when the user must be the discord_server's owner.
let(:user_is_owner) { false }
let(:discord_user) { build(:discord_user) }
let(:discord_server) do
build(
:discord_server,
:with_command_channels,
owner: user_is_owner ? discord_user : build(:discord_user)
)
end
let!(:community) { create(:community, discord_server: discord_server) }
let!(:user) do
args = respond_to?(:user_args) ? user_args : []
# When the user is the server owner, the discord_server factory has
# already registered them as a member; using :with_discord_member would
# double-register.
user =
if user_is_owner
create(:user, *args, discord_user: discord_user)
else
create(:user, :with_discord_member, *args, discord_user: discord_user, discord_server: discord_server)
end
community.update!(owner_user_id: user.id) if user_is_owner
user
end
let(:command_class) { described_class }
let(:command) { @previous_command || command_class.new }
let(:server) { create(:server, community_id: community.id) }
let(:second_user) { create(:user, :with_discord_member, discord_server: discord_server) }
let(:default_text_channel) { discord_server.channels.find { |channel| channel.name == "general" } || discord_server.channels.first }
#
# Executes the command as a user in a text or pm channel.
# The majority of the time, this method will be used along with the input arguments for the command
#
# @param channel_type [Symbol] Controls what type of channel messages are triggered in. Options: :text, :pm
# @param user [ESM::User] The user to send the message as. Defaults to the `user` let binding
# @param command_override [ESM::Command] The command to execute. Defaults to the `command` let binding
# @param channel [Discordrb::Channel, nil] The channel to execute the command in
# @param arguments [Hash] Any arguments the command is expecting as key: value pairs
# @param prompt_response [String, nil] Optional. The value to set as the user's "response" to ESM prompting them
# @param handle_error [TrueClass, FalseClass] Controls if errors should be handled by the command or bubbled up
# If this is true, the command's `.event_hook` method will be called. Any errors will be handled
# if this is false (default), the command's `#execute` method will be called. Any errors will raise in the specs
#
def execute!(**opts)
channel_type = opts.delete(:channel_type) || :text
send_as = opts.delete(:user) || user
command_class = opts.delete(:command_class) || self.command_class
arguments = opts.delete(:arguments) || {}
prompt_response = opts.delete(:prompt_response)
handle_error = opts.delete(:handle_error)
command = command_class.new
channel =
if (override = opts.delete(:channel))
override
elsif ESM::Command::Base::TEXT_CHANNEL_TYPES.include?(channel_type)
default_text_channel
else
send_as.discord_user.pm
end
channel = ESM.discord_bot.channel(channel) unless channel.is_a?(Discordrb::Channel)
data = {
id: "", # ID of interaction
application_id: "", # Bot id?
type: 2, # Interaction type. 2 is command
data: {
id: "", # Command ID
name: command.command_name # Command name
},
guild_id: channel.server&.id, # Server ID
channel_id: channel.id, # Channel ID
user: {
id: send_as.discord_id # User ID
},
token: ESM.config.token, # Bot token
version: 1 # IDK
}
if command.arguments.size > 0 && arguments.size > 0
options =
arguments.map do |key, value|
argument = command.arguments.template(key)
raise ArgumentError, "Invalid argument \"#{key}\" given for #{command.class}" if argument.nil?
value =
case value
when ESM::Server
value.server_id
when ESM::Community
value.community_id
when ESM::User
value.mention
else
# All values from Discord are sent as String
value.to_s
end
{name: argument.display_name.to_s, value: value, type: Discordrb::Interactions::OptionBuilder::TYPES[argument.type]}
end
data[:data][:options] = [{type: 1, name: command.command_name, options: options}]
end
respond_to_prompt(prompt_response, user: send_as, channel: channel) if prompt_response
event = Discordrb::Events::ApplicationCommandEvent.new(data.deep_stringify_keys, ESM.discord_bot)
# In normal operation, #event_hook will receive the ApplicationCommandEvent above
# SpecApplicationCommandEvent overwrites `#defer` and `#edit_response` to avoid
# sending those calls to Discord proper
if handle_error
# Allows commands to access this command after it has been used
@previous_command = command_class.event_hook(SpecApplicationCommandEvent.new(event))
return
end
event = ESM::Discord::Event::ApplicationCommand.new(event)
origin = ESM::Discord::Command::Origin.new(
user: ESM::User.from_discord(event.user),
community: ESM::Community.from_discord(event.server),
channel: event.channel
)
@previous_command = command_class.new(arguments: event.options, origin: origin)
ESM::Database.with_connection do
@previous_command.from_discord!
end
end
def wait_for_completion!(event = :on_execute)
wait_for { previous_command.timers.public_send(event.to_sym).finished? }.to be(true)
end
def respond_to_prompt(response, user: discord_user, channel: default_text_channel)
discord_user_obj = user.respond_to?(:discord_user) ? user.discord_user : user
ESM.discord_bot.test_inbox.queue_reply(from: discord_user_obj, channel: channel, content: response)
end
def accept_request
previous_command.request.respond(true)
rescue => e
return if e.is_a?(ESM::Exception::CheckFailureNoMessage)
message =
if e.respond_to?(:to_embed)
e.to_embed
else
e.message
end
ESM.discord_bot.deliver(message, to: default_text_channel)
end
end