# frozen_string_literal: true module DiscordInteractions class YiffyAPI attr_reader :raw GREEN_TICK = APIKey::WebhookMethods::GREEN_TICK RED_TICK = APIKey::WebhookMethods::RED_TICK def initialize(raw) @raw = raw end def [](key) @raw[key.to_s] end # rubocop:disable Metrics/BlockNesting # noinspection RubyCaseWithoutElseBlockInspection def handle return { type: DiscordConstants::InteractionResponseTypes::PONG } if raw["type"] == DiscordConstants::InteractionTypes::PING if raw["guild_id"].nil? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "My commands cannot be used in Direct Messages.", **common, }, } end CurrentUser.user = CurrentUser.user = APIUser.find_or_create_by(id: raw["member"]["user"]["id"]) CurrentUser.ip_addr = "0.0.0.0" case raw["type"] when DiscordConstants::InteractionTypes::APPLICATION_COMMAND options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"]) sub = options.get_subcommand case raw["data"]["name"] when "apikey" keys = APIKey.for_owner(raw["member"]["user"]["id"]) case sub[0] when "create" if keys.length >= 3 return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "You already have the maximum amount of api keys. Contact a developer if you believe an exception should be made.", **common, }, } end { type: DiscordConstants::InteractionResponseTypes::MODAL, data: { custom_id: "apikey-create", components: [ { type: DiscordConstants::ComponentTypes::ACTION_ROW, components: [ { custom_id: "apikey-create.name", placeholder: "My Awesome Application", min_length: 3, max_length: 50, label: "Name", style: DiscordConstants::TextInputStyles::SHORT, type: DiscordConstants::ComponentTypes::TEXT_INPUT, }, ], }, { type: DiscordConstants::ComponentTypes::ACTION_ROW, components: [ { custom_id: "apikey-create.usage", placeholder: "Showing porn to my friends", min_length: 5, max_length: 400, label: "Usage", style: DiscordConstants::TextInputStyles::PARAGRAPH, type: DiscordConstants::ComponentTypes::TEXT_INPUT, }, ], }, ], title: "Create API Key", }, } when "delete" key = keys.find { |k| k.encoded == options.get_string("key", required: true) } if key.nil? || key.owner_id.to_s != raw["member"]["user"]["id"] return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Invalid key specified.", **common, }, } end if key.disabled? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "This key has been disabled by a developer. To have this key deleted or removed, concat a developer.\n\nDisable Reason: **#{key.disabled_reason || '(None)'}**", **common, }, } end { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Are you sure you want to delete the key **#{key.application_name}**? This action cannot be undone.", components: [ { type: DiscordConstants::ComponentTypes::ACTION_ROW, components: [ { custom_id: "apikey-delete-yes.#{key.encoded}.#{raw['member']['user']['id']}", label: "Yes", style: DiscordConstants::ButtonStyles::SUCCESS, type: DiscordConstants::ComponentTypes::BUTTON, }, { custom_id: "apikey-delete-no.#{raw['member']['user']['id']}", label: "No", style: DiscordConstants::ButtonStyles::DANGER, type: DiscordConstants::ComponentTypes::BUTTON, }, ], }, ], **common, }, } when "list" if keys.empty? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "You do not have any API keys.", **common, }, } end text = [] keys.each_with_index do |apikey, index| text.push( <<~TEXT, #{index + 1}.) - Key: ||#{apikey.key}|| - Application: `#{apikey.application_name}` - Usage: `#{apikey.usage || 'None Specified'}` - Active: #{apikey.active? ? GREEN_TICK : RED_TICK} - Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK} - Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK} - Services: #{apikey.access_string} TEXT ) end { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "We found the following api keys:\n\n#{text.join("\n\n")}", **common, }, } end when "apidev" unless Websites.config.yiffyapi_administrators.include?(raw["member"]["user"]["id"]) return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "You are not allowed to use that.", **common, }, } end case sub[0] when "list" user_id = options.get_user("user", required: true)["id"] keys = APIKey.for_owner(user_id) if keys.empty? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "That user does not have any API keys.", **common, }, } end text = [] keys.each_with_index do |apikey, index| text.push( <<~TEXT, #{index + 1}.) - Key: ||#{apikey.key}|| - Application: `#{apikey.application_name}` - Usage: `#{apikey.usage || 'None Specified'}` - Active: #{apikey.active? ? GREEN_TICK : RED_TICK} - Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK} - Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK} - Services: #{apikey.access_string} TEXT ) end { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "We found the following api keys for <@#{user_id}>:\n\n#{text.join("\n\n")}", **common, }, } when "disable" key = options.get_string("key", required: true) reason = options.get_string("reason") || "None Provided" apikey = APIKey.find_by(key: key) if apikey.nil? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "I couldn't find that api key.", **common, }, } end if apikey.disabled? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "That api key is already disabled.", **common, }, } end apikey.update(disabled: true, disabled_reason: reason) { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Api key successfully disabled.", **common, }, } when "enable" key = options.get_string("key", required: true) apikey = APIKey.find_by(key: key) if apikey.nil? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "I couldn't find that api key.", **common, }, } end unless apikey.disabled? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "That api key is not disabled.", **common, }, } end apikey.update(disabled: false, disabled_reason: nil) { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Api key successfully enabled.", **common, }, } when "stats" key = options.get_string("key") ip = options.get_string("ip") if key apikey = APIKey.find_by(key: key) if apikey.nil? return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "I couldn't find that api key.", **common, }, } end end stats = APIKey.stats(ip: ip, key: apikey) if key.present? stats = flatten_hash(stats[:key]) text = "Stats for **#{apikey.application_name}** (||#{apikey.key}||)\n\n" elsif ip.present? stats = flatten_hash(stats[:ip]) text = "Stats for **#{ip}**\n\n" else stats = flatten_hash(stats[:global]) text = "Global stats\n\n" end stats.each do |name, value| text += "**#{name}**: #{ActiveSupport::NumberHelper.number_to_delimited(value)}\n" end { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: text, **common, }, } end end when DiscordConstants::InteractionTypes::MESSAGE_COMPONENT action, *other, user = raw["data"]["custom_id"].split(".") if user != raw["member"]["user"]["id"] return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "That is not yours to play with.", **common, }, } end case action when "apikey-delete-yes" key = APIKey.for_owner(user).find { |k| k.encoded == other[0] } if key.nil? || key.owner_id.to_s != user return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Invalid key specified.", **common, }, } end key.destroy { type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE, data: { content: "Key deleted.", components: [], **common, }, } when "apikey-delete-no" { type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE, data: { content: "Cancelled.", components: [], **common, }, } end when DiscordConstants::InteractionTypes::APPLICATION_COMMAND_AUTOCOMPLETE options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"]) sub, = options.get_subcommand opt = options.get_focused_option(required: true) case raw["data"]["name"] when "apikey" case sub when "delete" keys = APIKey.for_owner(raw["member"]["user"]["id"]) value = opt["value"] keys = keys.filter { |k| k.application_name.start_with?(value) } { type: DiscordConstants::InteractionResponseTypes::APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, data: { choices: keys.map do |k| { name: k.application_name, value: k.encoded, } end, }, } end end when DiscordConstants::InteractionTypes::MODAL_SUBMIT case raw["data"]["custom_id"] when "apikey-create" name = raw["data"]["components"][0]["components"][0]["value"] usage = raw["data"]["components"][1]["components"][0]["value"] if name.length < 3 || name.length > 50 return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Name must be between 3 and 50 characters.", components: [], **common, }, } end if usage.length < 5 || usage.length > 400 return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Usage must be between 5 and 400 characters.", **common, }, } end key = APIKey.create( owner_id: raw["member"]["user"]["id"], application_name: name, usage: usage, ) if key.errors.present? Rails.logger.error("Failed to create apikey for #{raw['member']['user']['id']}: #{key.errors.full_messages.join(', ')}") return { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "An error occurred while creating your API key.", **common, }, } end { type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, data: { content: "Your API key: `#{key.key}`. Provide this in the `Authorization` header. You must still provide a unique user agent. If you have any issues, contact a developer.", **common, }, } end end end # rubocop:enable Metrics/BlockNesting private def flatten_hash(hash) hash.each_with_object({}) do |(k, v), h| if v.is_a?(Hash) flatten_hash(v).map do |h_k, h_v| h[:"#{k}.#{h_k}"] = h_v end else h[k] = v end end end def common { flags: DiscordConstants::MessageFlags::EPHEMERAL, allowed_mentions: { users: [], roles: [], everyone: [], replied_user: false, }, } end end end