Websites/app/logical/discord_interactions/yiffyapi.rb

481 lines
17 KiB
Ruby

# 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